mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into relational-sort
This commit is contained in:
18
api/package-lock.json
generated
18
api/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -6475,6 +6475,11 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7505,6 +7510,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@@ -10396,9 +10406,9 @@
|
||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||
"version": "6.9.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
|
||||
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
@@ -106,6 +106,7 @@
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"macos-release": "^2.4.1",
|
||||
"mime-types": "^2.1.27",
|
||||
"ms": "^2.1.2",
|
||||
"nanoid": "^3.1.12",
|
||||
"node-machine-id": "^1.1.12",
|
||||
@@ -115,6 +116,7 @@
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "^6.4.1",
|
||||
"pino-colada": "^2.1.0",
|
||||
"qs": "^6.9.4",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
|
||||
172
api/src/app.ts
172
api/src/app.ts
@@ -14,7 +14,6 @@ import rateLimiter from './middleware/rate-limiter';
|
||||
import cache from './middleware/cache';
|
||||
import extractToken from './middleware/extract-token';
|
||||
import authenticate from './middleware/authenticate';
|
||||
import responseHook from './middleware/response-hook';
|
||||
import activityRouter from './controllers/activity';
|
||||
import assetsRouter from './controllers/assets';
|
||||
import authRouter from './controllers/auth';
|
||||
@@ -45,99 +44,100 @@ import { InvalidPayloadException } from './exceptions';
|
||||
import { registerExtensions } from './extensions';
|
||||
import emitter from './emitter';
|
||||
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
export default async function createApp() {
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
|
||||
const app = express();
|
||||
const app = express();
|
||||
|
||||
const customRouter = express.Router();
|
||||
const customRouter = express.Router();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', true);
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', true);
|
||||
|
||||
app.use(expressLogger({ logger }));
|
||||
app.use(responseHook);
|
||||
app.use(expressLogger({ logger }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
bodyParser.json()(req, res, (err) => {
|
||||
if (err) {
|
||||
return next(new InvalidPayloadException(err.message));
|
||||
}
|
||||
app.use((req, res, next) => {
|
||||
bodyParser.json()(req, res, (err) => {
|
||||
if (err) {
|
||||
return next(new InvalidPayloadException(err.message));
|
||||
}
|
||||
|
||||
return next();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.use(bodyParser.json());
|
||||
app.use(extractToken);
|
||||
app.use(bodyParser.json());
|
||||
app.use(extractToken);
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('X-Powered-By', 'Directus');
|
||||
next();
|
||||
});
|
||||
|
||||
if (env.CORS_ENABLED === true) {
|
||||
app.use(cors);
|
||||
}
|
||||
|
||||
if (env.NODE_ENV !== 'development') {
|
||||
const adminPath = require.resolve('@directus/app/dist/index.html');
|
||||
|
||||
app.get('/', (req, res) => res.redirect('/admin/'));
|
||||
app.use('/admin', express.static(path.join(adminPath, '..')));
|
||||
app.use('/admin/*', (req, res) => {
|
||||
res.sendFile(adminPath);
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('X-Powered-By', 'Directus');
|
||||
next();
|
||||
});
|
||||
|
||||
if (env.CORS_ENABLED === true) {
|
||||
app.use(cors);
|
||||
}
|
||||
|
||||
if (env.NODE_ENV !== 'development') {
|
||||
const adminPath = require.resolve('@directus/app/dist/index.html');
|
||||
|
||||
app.get('/', (req, res) => res.redirect('/admin/'));
|
||||
app.use('/admin', express.static(path.join(adminPath, '..')));
|
||||
app.use('/admin/*', (req, res) => {
|
||||
res.sendFile(adminPath);
|
||||
});
|
||||
}
|
||||
|
||||
// use the rate limiter - all routes for now
|
||||
if (env.RATE_LIMITER_ENABLED === true) {
|
||||
app.use(rateLimiter);
|
||||
}
|
||||
|
||||
app.use(sanitizeQuery);
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
app.use(authenticate);
|
||||
|
||||
app.use(checkIP);
|
||||
|
||||
app.use(cache);
|
||||
|
||||
app.use('/graphql', graphqlRouter);
|
||||
|
||||
app.use('/activity', activityRouter);
|
||||
app.use('/assets', assetsRouter);
|
||||
app.use('/collections', collectionsRouter);
|
||||
app.use('/extensions', extensionsRouter);
|
||||
app.use('/fields', fieldsRouter);
|
||||
app.use('/files', filesRouter);
|
||||
app.use('/folders', foldersRouter);
|
||||
app.use('/items', itemsRouter);
|
||||
app.use('/permissions', permissionsRouter);
|
||||
app.use('/presets', presetsRouter);
|
||||
app.use('/relations', relationsRouter);
|
||||
app.use('/revisions', revisionsRouter);
|
||||
app.use('/roles', rolesRouter);
|
||||
app.use('/server/', serverRouter);
|
||||
app.use('/settings', settingsRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use('/utils', utilsRouter);
|
||||
app.use('/webhooks', webhooksRouter);
|
||||
app.use('/custom', customRouter);
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Register all webhooks
|
||||
const webhooksService = new WebhooksService();
|
||||
await webhooksService.register();
|
||||
|
||||
// Register custom hooks / endpoints
|
||||
await registerExtensions(customRouter);
|
||||
|
||||
track('serverStarted');
|
||||
|
||||
emitter.emit('init.before', { app });
|
||||
emitter.emitAsync('init').catch((err) => logger.warn(err));
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
// use the rate limiter - all routes for now
|
||||
if (env.RATE_LIMITER_ENABLED === true) {
|
||||
app.use(rateLimiter);
|
||||
}
|
||||
|
||||
app.use(sanitizeQuery);
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
app.use(authenticate);
|
||||
|
||||
app.use(checkIP);
|
||||
|
||||
app.use(cache);
|
||||
|
||||
app.use('/graphql', graphqlRouter);
|
||||
|
||||
app.use('/activity', activityRouter);
|
||||
app.use('/assets', assetsRouter);
|
||||
app.use('/collections', collectionsRouter);
|
||||
app.use('/extensions', extensionsRouter);
|
||||
app.use('/fields', fieldsRouter);
|
||||
app.use('/files', filesRouter);
|
||||
app.use('/folders', foldersRouter);
|
||||
app.use('/items', itemsRouter);
|
||||
app.use('/permissions', permissionsRouter);
|
||||
app.use('/presets', presetsRouter);
|
||||
app.use('/relations', relationsRouter);
|
||||
app.use('/revisions', revisionsRouter);
|
||||
app.use('/roles', rolesRouter);
|
||||
app.use('/server/', serverRouter);
|
||||
app.use('/settings', settingsRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use('/utils', utilsRouter);
|
||||
app.use('/webhooks', webhooksRouter);
|
||||
app.use('/custom', customRouter);
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Register all webhooks
|
||||
const webhooksService = new WebhooksService();
|
||||
webhooksService.register();
|
||||
|
||||
// Register custom hooks / endpoints
|
||||
registerExtensions(customRouter);
|
||||
|
||||
track('serverStarted');
|
||||
|
||||
emitter.emit('init.before', { app });
|
||||
emitter.emitAsync('init').catch((err) => logger.warn(err));
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -119,6 +119,10 @@ router.get(
|
||||
res.attachment(file.filename_download);
|
||||
res.setHeader('Content-Type', file.type);
|
||||
|
||||
if (req.query.hasOwnProperty('download') === false) {
|
||||
res.removeHeader('Content-Disposition');
|
||||
}
|
||||
|
||||
stream.pipe(res);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -60,7 +60,6 @@ const newFieldSchema = Joi.object({
|
||||
field: Joi.string().required(),
|
||||
type: Joi.string().valid(...types, null),
|
||||
schema: Joi.object({
|
||||
comment: Joi.string().allow(null),
|
||||
default_value: Joi.any(),
|
||||
max_length: [Joi.number(), Joi.string(), Joi.valid(null)],
|
||||
is_nullable: Joi.bool(),
|
||||
|
||||
@@ -42,6 +42,8 @@ columns:
|
||||
column: id
|
||||
modified_on:
|
||||
type: timestamp
|
||||
nullable: false
|
||||
default: '$now'
|
||||
charset:
|
||||
type: string
|
||||
length: 50
|
||||
|
||||
@@ -31,7 +31,7 @@ data:
|
||||
cards:
|
||||
icon: account_circle
|
||||
title: '{{ first_name }} {{ last_name }}'
|
||||
subtitle: '{{ title }}'
|
||||
subtitle: '{{ email }}'
|
||||
size: 4
|
||||
|
||||
- collection: directus_activity
|
||||
|
||||
@@ -99,8 +99,9 @@ async function createTables(database: Knex) {
|
||||
if (columnInfo.default !== undefined) {
|
||||
let defaultValue = columnInfo.default;
|
||||
|
||||
if (isObject(defaultValue) || Array.isArray(defaultValue))
|
||||
if (isObject(defaultValue) || Array.isArray(defaultValue)) {
|
||||
defaultValue = JSON.stringify(defaultValue);
|
||||
}
|
||||
|
||||
if (defaultValue === '$now') {
|
||||
defaultValue = database!.fn.now();
|
||||
|
||||
@@ -72,7 +72,7 @@ function registerHooks(hooks: string[]) {
|
||||
registerHook(hook);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register hook "${hook}"`);
|
||||
logger.info(error);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ function registerEndpoints(endpoints: string[], router: Router) {
|
||||
registerEndpoint(endpoint);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register endpoint "${endpoint}"`);
|
||||
logger.info(error);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import emitter from '../emitter';
|
||||
import logger from '../logger';
|
||||
|
||||
const responseHook: RequestHandler = asyncHandler((req, res, next) => {
|
||||
res.on('close', afterResponse);
|
||||
|
||||
const startTime = process.hrtime();
|
||||
|
||||
return next();
|
||||
|
||||
function afterResponse() {
|
||||
res.removeListener('close', afterResponse);
|
||||
|
||||
const info = {
|
||||
request: {
|
||||
method: req.method,
|
||||
uri: req.path,
|
||||
url: req.protocol + '://' + req.get('host') + req.originalUrl,
|
||||
size: req.socket.bytesRead,
|
||||
query: req.query,
|
||||
headers: req.headers,
|
||||
},
|
||||
response: {
|
||||
status: res.statusCode,
|
||||
size: (res as any)['_contentLength'] || res.getHeader('content-length'),
|
||||
headers: res.getHeaders(),
|
||||
},
|
||||
ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress,
|
||||
duration: (process.hrtime(startTime)[1] / 1000000).toFixed(),
|
||||
};
|
||||
|
||||
emitter.emitAsync('response', info).catch((err) => logger.warn(err));
|
||||
}
|
||||
});
|
||||
|
||||
export default responseHook;
|
||||
@@ -1,43 +1,113 @@
|
||||
import logger from './logger';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import qs from 'qs';
|
||||
import { URL } from 'url';
|
||||
import { createTerminus, TerminusOptions } from '@godaddy/terminus';
|
||||
import http from 'http';
|
||||
import { Request } from 'express';
|
||||
import logger from './logger';
|
||||
import emitter from './emitter';
|
||||
import database from './database';
|
||||
import app from './app';
|
||||
import createApp from './app';
|
||||
import { once } from 'lodash';
|
||||
|
||||
const server = http.createServer(app);
|
||||
export default async function createServer() {
|
||||
const server = http.createServer(await createApp());
|
||||
|
||||
const terminusOptions: TerminusOptions = {
|
||||
timeout: 1000,
|
||||
signals: ['SIGINT', 'SIGTERM', 'SIGHUP'],
|
||||
beforeShutdown,
|
||||
onSignal,
|
||||
onShutdown,
|
||||
};
|
||||
server.on('request', function (req: http.IncomingMessage & Request, res: http.ServerResponse) {
|
||||
const startTime = process.hrtime();
|
||||
|
||||
createTerminus(server, terminusOptions);
|
||||
const complete = once(function (finished: boolean) {
|
||||
const elapsedTime = process.hrtime(startTime);
|
||||
const elapsedNanoseconds = elapsedTime[0] * 1e9 + elapsedTime[1];
|
||||
const elapsedMilliseconds = elapsedNanoseconds / 1e6;
|
||||
|
||||
export default server;
|
||||
const previousIn = (req.connection as any)._metrics?.in || 0;
|
||||
const previousOut = (req.connection as any)._metrics?.out || 0;
|
||||
|
||||
async function beforeShutdown() {
|
||||
await emitter.emitAsync('server.stop.before', { server });
|
||||
const metrics = {
|
||||
in: req.connection.bytesRead - previousIn,
|
||||
out: req.connection.bytesWritten - previousOut,
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
logger.info('Restarting...');
|
||||
} else {
|
||||
logger.info('Shutting down...');
|
||||
}
|
||||
}
|
||||
|
||||
async function onSignal() {
|
||||
await database.destroy();
|
||||
logger.info('Database connections destroyed');
|
||||
}
|
||||
|
||||
async function onShutdown() {
|
||||
emitter.emitAsync('server.stop').catch((err) => logger.warn(err));
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
logger.info('Directus shut down OK. Bye bye!');
|
||||
(req.connection as any)._metrics = {
|
||||
in: req.connection.bytesRead,
|
||||
out: req.connection.bytesWritten,
|
||||
};
|
||||
|
||||
// Compatibility when supporting serving with certificates
|
||||
const protocol = server instanceof https.Server ? 'https' : 'http';
|
||||
|
||||
const url = new URL(
|
||||
(req.originalUrl || req.url) as string,
|
||||
`${protocol}://${req.headers.host}`
|
||||
);
|
||||
const query = url.search.startsWith('?') ? url.search.substr(1) : url.search;
|
||||
|
||||
const info = {
|
||||
finished,
|
||||
request: {
|
||||
aborted: req.aborted,
|
||||
completed: req.complete,
|
||||
method: req.method,
|
||||
url: url.href,
|
||||
path: url.pathname,
|
||||
protocol,
|
||||
host: req.headers.host,
|
||||
size: metrics.in,
|
||||
query: qs.parse(query),
|
||||
headers: req.headers,
|
||||
},
|
||||
response: {
|
||||
status: res.statusCode,
|
||||
size: metrics.out,
|
||||
headers: res.getHeaders(),
|
||||
},
|
||||
ip:
|
||||
req.headers['x-forwarded-for'] ||
|
||||
req.connection?.remoteAddress ||
|
||||
req.socket?.remoteAddress,
|
||||
duration: elapsedMilliseconds.toFixed(),
|
||||
};
|
||||
|
||||
emitter.emitAsync('response', info).catch((err) => logger.warn(err));
|
||||
});
|
||||
|
||||
res.once('finish', complete.bind(null, true));
|
||||
res.once('close', complete.bind(null, false));
|
||||
});
|
||||
|
||||
const terminusOptions: TerminusOptions = {
|
||||
timeout: 1000,
|
||||
signals: ['SIGINT', 'SIGTERM', 'SIGHUP'],
|
||||
beforeShutdown,
|
||||
onSignal,
|
||||
onShutdown,
|
||||
};
|
||||
|
||||
createTerminus(server, terminusOptions);
|
||||
|
||||
return server;
|
||||
|
||||
async function beforeShutdown() {
|
||||
await emitter.emitAsync('server.stop.before', { server });
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
logger.info('Restarting...');
|
||||
} else {
|
||||
logger.info('Shutting down...');
|
||||
}
|
||||
}
|
||||
|
||||
async function onSignal() {
|
||||
await database.destroy();
|
||||
logger.info('Database connections destroyed');
|
||||
}
|
||||
|
||||
async function onShutdown() {
|
||||
emitter.emitAsync('server.stop').catch((err) => logger.warn(err));
|
||||
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
logger.info('Directus shut down OK. Bye bye!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +260,13 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
if (field.schema.default_value) {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
const defaultValue = field.schema.default_value.toLowerCase();
|
||||
|
||||
if (defaultValue === 'now()') {
|
||||
column.defaultTo(this.knex.fn.now());
|
||||
} else {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.schema.is_nullable !== undefined && field.schema.is_nullable === false) {
|
||||
|
||||
@@ -4,12 +4,13 @@ import sharp from 'sharp';
|
||||
import { parse as parseICC } from 'icc';
|
||||
import parseEXIF from 'exif-reader';
|
||||
import parseIPTC from '../utils/parse-iptc';
|
||||
import path from 'path';
|
||||
import { AbstractServiceOptions, File, PrimaryKey } from '../types';
|
||||
import { clone } from 'lodash';
|
||||
import cache from '../cache';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { extension } from 'mime-types';
|
||||
import path from 'path';
|
||||
|
||||
export class FilesService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
@@ -37,7 +38,10 @@ export class FilesService extends ItemsService {
|
||||
primaryKey = await this.create(payload);
|
||||
}
|
||||
|
||||
payload.filename_disk = primaryKey + path.extname(payload.filename_download);
|
||||
const fileExtension =
|
||||
(payload.type && extension(payload.type)) || path.extname(payload.filename_download);
|
||||
|
||||
payload.filename_disk = primaryKey + fileExtension;
|
||||
|
||||
if (!payload.type) {
|
||||
payload.type = 'application/octet-stream';
|
||||
|
||||
@@ -24,6 +24,7 @@ import { AuthorizationService } from './authorization';
|
||||
import { pick, clone, cloneDeep } from 'lodash';
|
||||
import getDefaultValue from '../utils/get-default-value';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
|
||||
export class ItemsService implements AbstractService {
|
||||
collection: string;
|
||||
@@ -256,6 +257,9 @@ export class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
const result = await runAST(ast, { knex: this.knex });
|
||||
|
||||
if (result === null) throw new ForbiddenException();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ if (require.main === module) {
|
||||
}
|
||||
|
||||
export default async function start() {
|
||||
const server = require('./server').default;
|
||||
const createServer = require('./server').default;
|
||||
const { validateDBConnection } = require('./database');
|
||||
|
||||
const server = await createServer();
|
||||
|
||||
await validateDBConnection();
|
||||
|
||||
await emitter.emitAsync('server.start.before', { server });
|
||||
|
||||
164
app/package-lock.json
generated
164
app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -6601,6 +6601,51 @@
|
||||
"tslint": "^5.20.1",
|
||||
"webpack": "^4.0.0",
|
||||
"yorkie": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/cli-plugin-unit-jest": {
|
||||
@@ -6740,6 +6785,17 @@
|
||||
"unique-filename": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
@@ -6823,6 +6879,18 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@@ -6936,6 +7004,18 @@
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
@@ -11664,51 +11744,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
@@ -20342,43 +20377,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-loader-v16": {
|
||||
"version": "npm:vue-loader@16.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-oouKUQWWHbSihqSD7mhymGPX1OQ4hedzAHyvm8RdyHh6m3oIvoRF+NM45i/bhNOlo8jCnuJhaSUf/6oDjv978g==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"loader-utils": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
"json5": "^2.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.4.6.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/app",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"private": false,
|
||||
"description": "Directus is an Open-Source Headless CMS & API for Managing Custom Databases",
|
||||
"author": "Rijk van Zanten <rijk@rngr.org>",
|
||||
|
||||
@@ -125,9 +125,42 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
::v-deep .v-card {
|
||||
--v-card-min-width: 540px;
|
||||
--v-card-min-width: calc(100vw - 40px);
|
||||
--v-card-padding: 20px;
|
||||
--v-card-background-color: var(--background-page);
|
||||
|
||||
.v-card-actions {
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column-reverse;
|
||||
& > .v-button + .v-button {
|
||||
margin-left: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.v-button {
|
||||
width: 100%;
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(small) {
|
||||
--v-card-min-width: 540px;
|
||||
.v-card-actions {
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: inherit;
|
||||
& > .v-button + .v-button {
|
||||
margin-left: 12px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.v-button {
|
||||
width: auto;
|
||||
.button {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .v-sheet {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<template #activator>{{ field.name || formatTitle(field.field) }}</template>
|
||||
<field-list-item
|
||||
v-for="childField in field.children"
|
||||
:key="childField.field"
|
||||
:key="field.field + childField.field"
|
||||
:parent="`${parent ? parent + '.' : ''}${field.field}`"
|
||||
:field="childField"
|
||||
:depth="depth - 1"
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
<template>
|
||||
<v-list>
|
||||
<v-list-item :disabled="value === null" @click="$emit('input', null)">
|
||||
<v-list-item
|
||||
v-if="defaultValue === null || !isRequired"
|
||||
:disabled="value === null"
|
||||
@click="$emit('input', null)"
|
||||
>
|
||||
<v-list-item-icon><v-icon name="delete_outline" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('clear_value') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item @click="$emit('input', defaultValue)">
|
||||
<v-list-item
|
||||
v-if="defaultValue !== null"
|
||||
:disabled="value === defaultValue"
|
||||
@click="$emit('input', defaultValue)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="settings_backup_restore" />
|
||||
</v-list-item-icon>
|
||||
@@ -52,7 +60,11 @@ export default defineComponent({
|
||||
return savedValue !== undefined ? savedValue : null;
|
||||
});
|
||||
|
||||
return { defaultValue };
|
||||
const isRequired = computed(() => {
|
||||
return props.field?.schema?.is_nullable === false;
|
||||
});
|
||||
|
||||
return { defaultValue, isRequired };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,6 +20,8 @@ type Query = {
|
||||
export function useItems(collection: Ref<string>, query: Query) {
|
||||
const { primaryKeyField, sortField } = useCollection(collection);
|
||||
|
||||
let loadingTimeout: any = null;
|
||||
|
||||
const { limit, fields, sort, page, filters, searchQuery } = query;
|
||||
|
||||
const endpoint = computed(() => {
|
||||
@@ -103,9 +105,11 @@ export function useItems(collection: Ref<string>, query: Query) {
|
||||
return { itemCount, totalCount, items, totalPages, loading, error, changeManualSort, getItems };
|
||||
|
||||
async function getItems() {
|
||||
if (loadingTimeout) return;
|
||||
|
||||
error.value = null;
|
||||
|
||||
const loadingTimeout = setTimeout(() => {
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loading.value = true;
|
||||
}, 250);
|
||||
|
||||
@@ -182,6 +186,7 @@ export function useItems(collection: Ref<string>, query: Query) {
|
||||
error.value = err;
|
||||
} finally {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export default defineComponent({
|
||||
required: true,
|
||||
validator: (val: string) => ['dateTime', 'date', 'time', 'timestamp'].includes(val),
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'long',
|
||||
},
|
||||
relative: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -57,9 +61,18 @@ export default defineComponent({
|
||||
addSuffix: true,
|
||||
});
|
||||
} else {
|
||||
let format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
let format;
|
||||
if (props.format === 'long') {
|
||||
format = `${i18n.t('date-fns_date')} ${i18n.t('date-fns_time')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time'));
|
||||
} else if (props.format === 'short') {
|
||||
format = `${i18n.t('date-fns_date_short')} ${i18n.t('date-fns_time_short')}`;
|
||||
if (props.type === 'date') format = String(i18n.t('date-fns_date_short'));
|
||||
if (props.type === 'time') format = String(i18n.t('date-fns_time_short'));
|
||||
} else {
|
||||
format = props.format;
|
||||
}
|
||||
|
||||
displayValue.value = await localizedFormat(newValue, format);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,32 @@ export default defineDisplay(({ i18n }) => ({
|
||||
icon: 'query_builder',
|
||||
handler: DisplayDateTime,
|
||||
options: [
|
||||
{
|
||||
field: 'format',
|
||||
name: i18n.t('displays.datetime.format'),
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
options: {
|
||||
choices: [
|
||||
{ text: i18n.t('displays.datetime.long'), value: 'long' },
|
||||
{ text: i18n.t('displays.datetime.short'), value: 'short' },
|
||||
],
|
||||
allowOther: true,
|
||||
},
|
||||
note: i18n.t('displays.datetime.format_note'),
|
||||
},
|
||||
schema: {
|
||||
default_value: 'long',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'relative',
|
||||
name: i18n.t('displays.datetime.relative'),
|
||||
type: 'boolean',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'toggle',
|
||||
options: {
|
||||
label: i18n.t('displays.datetime.relative_label'),
|
||||
|
||||
@@ -53,5 +53,5 @@ export default defineDisplay(({ i18n }) => ({
|
||||
},
|
||||
},
|
||||
],
|
||||
fields: ['id', 'avatar.id', 'first_name', 'last_name'],
|
||||
fields: ['id', 'avatar.id', 'email', 'first_name', 'last_name'],
|
||||
}));
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
v-if="(display === 'avatar' || display === 'both') && src"
|
||||
:src="src"
|
||||
role="presentation"
|
||||
:alt="value && `${value.first_name} ${value.last_name}`"
|
||||
:alt="value && userName(value)"
|
||||
:class="{ circle }"
|
||||
/>
|
||||
<img
|
||||
v-else-if="(display === 'avatar' || display === 'both') && src === null"
|
||||
src="../../assets/avatar-placeholder.svg"
|
||||
role="presentation"
|
||||
:alt="value && `${value.first_name} ${value.last_name}`"
|
||||
:alt="value && userName(value)"
|
||||
:class="{ circle }"
|
||||
/>
|
||||
<span v-if="display === 'name' || display === 'both'">{{ value.first_name }} {{ value.last_name }}</span>
|
||||
<span v-if="display === 'name' || display === 'both'">{{ userName(value) }}</span>
|
||||
</div>
|
||||
</user-popover>
|
||||
</template>
|
||||
@@ -23,12 +23,14 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
avatar: {
|
||||
id: string;
|
||||
};
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
@@ -58,7 +60,7 @@ export default defineComponent({
|
||||
return null;
|
||||
});
|
||||
|
||||
return { src };
|
||||
return { src, userName };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,11 +20,18 @@ export default function useRelation(collection: Ref<string>, field: Ref<string>)
|
||||
});
|
||||
|
||||
const junction = computed(() => {
|
||||
return relations.value.find((relation) => relation.one_collection === collection.value) as Relation;
|
||||
return relations.value.find(
|
||||
(relation) => relation.one_collection === collection.value && relation.one_field === field.value
|
||||
) as Relation;
|
||||
});
|
||||
|
||||
const relation = computed(() => {
|
||||
return relations.value.find((relation) => relation.one_collection !== collection.value) as Relation;
|
||||
return relations.value.find(
|
||||
(relation) =>
|
||||
relation.many_collection === junction.value.many_collection &&
|
||||
relation.many_field !== junction.value.many_field &&
|
||||
relation.many_field === junction.value.junction_field
|
||||
) as Relation;
|
||||
});
|
||||
|
||||
const junctionCollection = computed(() => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export default defineComponent({
|
||||
set(newVal: FieldMeta[] | null) {
|
||||
const fields = (newVal || []).map((meta: Record<string, any>) => ({
|
||||
field: meta.field,
|
||||
name: meta.field,
|
||||
name: meta.name || meta.field,
|
||||
type: meta.type,
|
||||
meta,
|
||||
}));
|
||||
@@ -53,6 +53,21 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const repeaterFields: DeepPartial<Field>[] = [
|
||||
{
|
||||
name: i18n.tc('name'),
|
||||
field: 'name',
|
||||
type: 'string',
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'full',
|
||||
sort: 1,
|
||||
options: {
|
||||
font: 'monospace',
|
||||
placeholder: i18n.t('interfaces.repeater.field_name_placeholder'),
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
{
|
||||
name: i18n.tc('field', 1),
|
||||
field: 'field',
|
||||
@@ -60,7 +75,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'half',
|
||||
sort: 1,
|
||||
sort: 2,
|
||||
options: {
|
||||
font: 'monospace',
|
||||
placeholder: i18n.t('interfaces.repeater.field_name_placeholder'),
|
||||
@@ -75,7 +90,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
sort: 2,
|
||||
sort: 3,
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
@@ -98,13 +113,13 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'dropdown',
|
||||
width: 'half',
|
||||
sort: 3,
|
||||
sort: 4,
|
||||
options: {
|
||||
choices: fieldTypes
|
||||
}
|
||||
choices: fieldTypes,
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'string'
|
||||
default_value: 'string',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -114,10 +129,10 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'interface',
|
||||
width: 'half',
|
||||
sort: 4,
|
||||
sort: 5,
|
||||
options: {
|
||||
typeField: 'type'
|
||||
}
|
||||
typeField: 'type',
|
||||
},
|
||||
},
|
||||
schema: null,
|
||||
},
|
||||
@@ -128,7 +143,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'text-input',
|
||||
width: 'full',
|
||||
sort: 5,
|
||||
sort: 6,
|
||||
options: {
|
||||
placeholder: i18n.t('interfaces.repeater.field_note_placeholder'),
|
||||
},
|
||||
@@ -142,7 +157,7 @@ export default defineComponent({
|
||||
meta: {
|
||||
interface: 'interface-options',
|
||||
width: 'full',
|
||||
sort: 6,
|
||||
sort: 7,
|
||||
options: {
|
||||
interfaceField: 'interface',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<v-item class="row" v-slot:default="{ active, toggle }" :active="true" :watch="false">
|
||||
<v-item class="row" v-slot:default="{ active, toggle }" :active="initialActive" :watch="false">
|
||||
<repeater-row-header
|
||||
:template="template"
|
||||
:value="value"
|
||||
@@ -39,6 +39,10 @@ export default defineComponent({
|
||||
type: Array as PropType<Partial<Field>[]>,
|
||||
default: () => [],
|
||||
},
|
||||
initialActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
@delete="removeItem(row)"
|
||||
:disabled="disabled"
|
||||
:headerPlaceholder="headerPlaceholder"
|
||||
:initialActive="addedIndex === index"
|
||||
/>
|
||||
</draggable>
|
||||
<button @click="addNew" class="add-new" v-if="showAddNew">
|
||||
@@ -32,7 +33,7 @@ export default defineComponent({
|
||||
components: { RepeaterRow, Draggable },
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
type: Array as PropType<Record<string, any>[]>,
|
||||
default: null,
|
||||
},
|
||||
fields: {
|
||||
@@ -61,7 +62,7 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const selection = ref<number[]>([]);
|
||||
const addedIndex = ref<number | null>(null);
|
||||
|
||||
const _template = computed(() => {
|
||||
if (props.template === null) return props.fields.length > 0 ? `{{${props.fields[0].field}}}` : '';
|
||||
@@ -76,11 +77,11 @@ export default defineComponent({
|
||||
return false;
|
||||
});
|
||||
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, selection, _template };
|
||||
return { updateValues, onSort, removeItem, addNew, showAddNew, hideDragImage, addedIndex, _template };
|
||||
|
||||
function updateValues(index: number, updatedValues: any) {
|
||||
emitValue(
|
||||
props.value.map((item, i) => {
|
||||
props.value.map((item: any, i: number) => {
|
||||
if (i === index) {
|
||||
return updatedValues;
|
||||
}
|
||||
@@ -95,7 +96,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function removeItem(row: any) {
|
||||
selection.value = [];
|
||||
addedIndex.value = null;
|
||||
if (props.value) {
|
||||
emitValue(props.value.filter((existingItem) => existingItem !== row));
|
||||
} else {
|
||||
@@ -111,6 +112,8 @@ export default defineComponent({
|
||||
newDefaults[field.field!] = field.schema?.default_value;
|
||||
});
|
||||
|
||||
addedIndex.value = props.value === null ? 0 : props.value.length;
|
||||
|
||||
if (props.value !== null) {
|
||||
emitValue([...props.value, newDefaults]);
|
||||
} else {
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
>
|
||||
<template #input v-if="currentUser">
|
||||
<div class="preview">
|
||||
<render-template
|
||||
collection="directus_users"
|
||||
:item="currentUser"
|
||||
:template="displayTemplate"
|
||||
/>
|
||||
{{ userName(currentUser) }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -65,7 +61,7 @@
|
||||
@click="setCurrent(item)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<render-template collection="directus_users" :template="displayTemplate" :item="item" />
|
||||
{{ userName(currentUser) }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -105,10 +101,6 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
template: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
selectMode: {
|
||||
type: String as PropType<'auto' | 'dropdown' | 'modal'>,
|
||||
default: 'auto',
|
||||
@@ -122,7 +114,7 @@ export default defineComponent({
|
||||
const { usesMenu, menuActive } = useMenu();
|
||||
const { info: collectionInfo } = useCollection(ref('directus_users'));
|
||||
const { selection, stageSelection, selectModalActive } = useSelection();
|
||||
const { displayTemplate, onPreviewClick, requiredFields } = usePreview();
|
||||
const { onPreviewClick } = usePreview();
|
||||
const { totalCount, loading: usersLoading, fetchUsers, users } = useUsers();
|
||||
|
||||
const { setCurrent, currentUser, loading: loadingCurrent, currentPrimaryKey } = useCurrent();
|
||||
@@ -134,7 +126,6 @@ export default defineComponent({
|
||||
return {
|
||||
collectionInfo,
|
||||
currentUser,
|
||||
displayTemplate,
|
||||
users,
|
||||
usersLoading,
|
||||
loadingCurrent,
|
||||
@@ -192,16 +183,10 @@ export default defineComponent({
|
||||
async function fetchCurrent() {
|
||||
loading.value = true;
|
||||
|
||||
const fields = requiredFields;
|
||||
|
||||
if (fields.includes('id') === false) {
|
||||
fields.push('id');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(`/users/${props.value}`, {
|
||||
params: {
|
||||
fields: fields,
|
||||
fields: ['id', 'email', 'first_name', 'last_name'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -231,11 +216,7 @@ export default defineComponent({
|
||||
|
||||
loading.value = true;
|
||||
|
||||
const fields = requiredFields;
|
||||
|
||||
if (fields.includes('id') === false) {
|
||||
fields.push('id');
|
||||
}
|
||||
const fields = ['id', 'email', 'first_name', 'last_name'];
|
||||
|
||||
try {
|
||||
const response = await api.get(`/users`, {
|
||||
@@ -280,10 +261,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function usePreview() {
|
||||
const displayTemplate = '{{ first_name }} {{ last_name }}';
|
||||
const requiredFields = ['first_name', 'last_name'];
|
||||
|
||||
return { onPreviewClick, displayTemplate, requiredFields };
|
||||
return { onPreviewClick };
|
||||
|
||||
function onPreviewClick() {
|
||||
if (props.disabled) return;
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"datetime": {
|
||||
"datetime": "Datetime",
|
||||
"description": "Display values related to time",
|
||||
"format": "Format",
|
||||
"format_note": "The custom format accetps the __[Date Field Symbol Table](https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)__",
|
||||
"long": "Long",
|
||||
"short": "Short",
|
||||
"relative": "Relative",
|
||||
"relative_label": "Show relative time, eg: 5 minutes ago"
|
||||
},
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
"create_user": "Create User",
|
||||
"create_webhook": "Create Webhook",
|
||||
|
||||
"invite_users": "Invite Users",
|
||||
"email_examples": "admin@example.com, user@example.com...",
|
||||
"invite": "Invite",
|
||||
"emails": "Emails",
|
||||
|
||||
"connection_excellent": "Excellent Connection",
|
||||
"connection_good": "Good Connection",
|
||||
"connection_fair": "Fair Connection",
|
||||
@@ -388,8 +393,9 @@
|
||||
|
||||
"date-fns_datetime": "PPP h:mma",
|
||||
"date-fns_date": "PPP",
|
||||
"date-fns_time": "h:mma",
|
||||
"date-fns_time": "h:mm:ss a",
|
||||
"date-fns_date_short": "MMM d, u",
|
||||
"date-fns_time_short": "h:mma",
|
||||
"date-fns_date_short_no_year": "MMM d",
|
||||
"month": "Month",
|
||||
"year": "Year",
|
||||
@@ -901,6 +907,8 @@
|
||||
"statuses_not_configured": "Status mapping configured incorrectly",
|
||||
"status_mapping": "Status Mapping",
|
||||
|
||||
"unknown_user": "Unknown User",
|
||||
|
||||
"creating_in": "Creating Item in {collection}",
|
||||
"editing_in": "Editing Item in {collection}",
|
||||
"editing_in_batch": "Batch Editing {count} Items",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<v-list-item to="/activity?action=comment" exact>
|
||||
<v-list-item-icon>
|
||||
<v-icon name="notes" />
|
||||
<v-icon name="chat_bubble_outline" />
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
{{ $t('comment') }}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- @TODO add final design -->
|
||||
<p class="type-label">User:</p>
|
||||
<user-popover v-if="item.user" :user="item.user.id">
|
||||
{{ item.user.first_name }} {{ item.user.last_name }}
|
||||
{{ userName(item.user) }}
|
||||
</user-popover>
|
||||
|
||||
<p class="type-label">Action:</p>
|
||||
@@ -51,6 +51,7 @@ import { defineComponent, computed, toRefs, ref, watch } from '@vue/composition-
|
||||
import { i18n } from '@/lang';
|
||||
import router from '@/router';
|
||||
import api from '@/api';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -58,6 +59,7 @@ type Values = {
|
||||
|
||||
type ActivityRecord = {
|
||||
user: {
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
} | null;
|
||||
@@ -97,6 +99,7 @@ export default defineComponent({
|
||||
error,
|
||||
close,
|
||||
openItemLink,
|
||||
userName,
|
||||
};
|
||||
|
||||
async function loadActivity() {
|
||||
@@ -107,6 +110,7 @@ export default defineComponent({
|
||||
params: {
|
||||
fields: [
|
||||
'user.id',
|
||||
'user.email',
|
||||
'user.first_name',
|
||||
'user.last_name',
|
||||
'action',
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<template #title v-else-if="isNew === false && isBatch === false && collectionInfo.meta.display_template">
|
||||
<template
|
||||
#title
|
||||
v-else-if="
|
||||
isNew === false && isBatch === false && collectionInfo.meta && collectionInfo.meta.display_template
|
||||
"
|
||||
>
|
||||
<v-skeleton-loader class="title-loader" type="text" v-if="loading" />
|
||||
|
||||
<h1 class="type-title" v-else>
|
||||
|
||||
@@ -115,6 +115,7 @@ import i18n from '@/lang';
|
||||
import marked from 'marked';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
import api from '@/api';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
inheritAttrs: false,
|
||||
@@ -194,31 +195,31 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.get(`/users/${props.file.uploaded_by}`, {
|
||||
params: {
|
||||
fields: ['id', 'first_name', 'last_name', 'role'],
|
||||
fields: ['id', 'email', 'first_name', 'last_name', 'role'],
|
||||
},
|
||||
});
|
||||
|
||||
const { id, first_name, last_name, role } = response.data.data;
|
||||
const user = response.data.data;
|
||||
|
||||
userCreated.value = {
|
||||
id: props.file.uploaded_by,
|
||||
name: first_name + ' ' + last_name,
|
||||
link: `/users/${id}`,
|
||||
name: userName(user),
|
||||
link: `/users/${user.id}`,
|
||||
};
|
||||
|
||||
if (props.file.modified_by) {
|
||||
const response = await api.get(`/users/${props.file.modified_by}`, {
|
||||
params: {
|
||||
fields: ['id', 'first_name', 'last_name', 'role'],
|
||||
fields: ['id', 'email', 'first_name', 'last_name', 'role'],
|
||||
},
|
||||
});
|
||||
|
||||
const { id, first_name, last_name, role } = response.data.data;
|
||||
const user = response.data.data;
|
||||
|
||||
userModified.value = {
|
||||
id: props.file.modified_by,
|
||||
name: first_name + ' ' + last_name,
|
||||
link: `/users/${id}`,
|
||||
name: userName(user),
|
||||
link: `/users/${user.id}`,
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item to="/files/mine" exact>
|
||||
<v-list-item-icon><v-icon name="face" /></v-list-item-icon>
|
||||
<v-list-item-icon><v-icon name="folder_shared" /></v-list-item-icon>
|
||||
<v-list-item-content>{{ $t('my_files') }}</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
const filePath = getRootPath() + `assets/${props.primaryKey}`;
|
||||
const filePath = getRootPath() + `assets/${props.primaryKey}?download`;
|
||||
window.open(filePath, '_blank');
|
||||
}
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ import { getInterfaces } from '@/interfaces';
|
||||
import router from '@/router';
|
||||
import notify from '@/utils/notify';
|
||||
import { i18n } from '@/lang';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getLocalTypeForField } from '../../get-local-type';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -323,8 +324,8 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
async function saveDuplicate() {
|
||||
const newField: any = {
|
||||
...props.field,
|
||||
const newField: Record<string, any> = {
|
||||
...cloneDeep(props.field),
|
||||
field: duplicateName.value,
|
||||
collection: duplicateTo.value,
|
||||
};
|
||||
@@ -334,6 +335,10 @@ export default defineComponent({
|
||||
delete newField.meta.sort;
|
||||
}
|
||||
|
||||
if (newField.schema) {
|
||||
delete newField.schema.comment;
|
||||
}
|
||||
|
||||
delete newField.name;
|
||||
|
||||
duplicating.value = true;
|
||||
@@ -532,6 +537,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.required {
|
||||
position: relative;
|
||||
left: -8px;
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -373,7 +373,6 @@ export default defineComponent({
|
||||
special: ['user-created'],
|
||||
interface: 'user',
|
||||
options: {
|
||||
template: '{{first_name}} {{last_name}}',
|
||||
display: 'both',
|
||||
},
|
||||
readonly: true,
|
||||
@@ -407,7 +406,6 @@ export default defineComponent({
|
||||
special: ['user-updated'],
|
||||
interface: 'user',
|
||||
options: {
|
||||
template: '{{first_name}} {{last_name}}',
|
||||
display: 'both',
|
||||
},
|
||||
readonly: true,
|
||||
|
||||
@@ -103,11 +103,12 @@ import { TranslateResult } from 'vue-i18n';
|
||||
import router from '@/router';
|
||||
import ValueNull from '@/views/private/components/value-null';
|
||||
import PresetsInfoSidebarDetail from './components/presets-info-sidebar-detail.vue';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type PresetRaw = {
|
||||
id: number;
|
||||
bookmark: null | string;
|
||||
user: null | { first_name: string; last_name: string };
|
||||
user: null | { email: string; first_name: string; last_name: string };
|
||||
role: null | { name: string };
|
||||
collection: string;
|
||||
layout: string;
|
||||
@@ -173,7 +174,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (preset.user) {
|
||||
scope = `${preset.user.first_name} ${preset.user.last_name}`;
|
||||
scope = userName(preset.user);
|
||||
}
|
||||
|
||||
const collection = collectionsStore.getCollection(preset.collection)?.name;
|
||||
@@ -200,6 +201,7 @@ export default defineComponent({
|
||||
fields: [
|
||||
'id',
|
||||
'bookmark',
|
||||
'user.email',
|
||||
'user.first_name',
|
||||
'user.last_name',
|
||||
'role.name',
|
||||
|
||||
@@ -102,6 +102,7 @@ import { useCollectionsStore, usePresetsStore } from '@/stores';
|
||||
import { getLayouts } from '@/layouts';
|
||||
import router from '@/router';
|
||||
import marked from 'marked';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
@@ -365,12 +366,12 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.get(`/users`, {
|
||||
params: {
|
||||
fields: ['first_name', 'last_name', 'id'],
|
||||
fields: ['email', 'first_name', 'last_name', 'id'],
|
||||
},
|
||||
});
|
||||
|
||||
users.value = response.data.data.map((user: any) => ({
|
||||
name: user.first_name + ' ' + user.last_name,
|
||||
name: userName(user),
|
||||
id: user.id,
|
||||
}));
|
||||
} catch (err) {
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button rounded icon @click="userInviteModalActive = true" v-tooltip.bottom="$t('invite_users')">
|
||||
<v-icon name="person_add" />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
@@ -55,6 +59,8 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<users-invite v-model="userInviteModalActive" :role="primaryKey" />
|
||||
|
||||
<div class="roles">
|
||||
<v-notice v-if="adminEnabled" type="info">
|
||||
{{ $t('admins_have_all_permissions') }}
|
||||
@@ -85,9 +91,11 @@ import SettingsNavigation from '../../../components/navigation.vue';
|
||||
import router from '@/router';
|
||||
import RevisionsDrawerDetail from '@/views/private/components/revisions-drawer-detail';
|
||||
import useItem from '@/composables/use-item';
|
||||
import { useUserStore } from '@/stores/';
|
||||
import { useUserStore, usePermissionsStore } from '@/stores/';
|
||||
import RoleInfoSidebarDetail from './components/role-info-sidebar-detail.vue';
|
||||
import PermissionsOverview from './components/permissions-overview.vue';
|
||||
import UsersInvite from '@/views/private/components/users-invite';
|
||||
import usersCreate from '../../../../../../../api/dist/cli/commands/users/create';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -95,7 +103,7 @@ type Values = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'roles-item',
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoSidebarDetail, PermissionsOverview },
|
||||
components: { SettingsNavigation, RevisionsDrawerDetail, RoleInfoSidebarDetail, PermissionsOverview, UsersInvite },
|
||||
props: {
|
||||
primaryKey: {
|
||||
type: String,
|
||||
@@ -108,7 +116,8 @@ export default defineComponent({
|
||||
},
|
||||
setup(props) {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const permissionsStore = usePermissionsStore();
|
||||
const userInviteModalActive = ref(false);
|
||||
const { primaryKey } = toRefs(props);
|
||||
|
||||
const { edits, item, saving, loading, error, save, remove, deleting, isBatch } = useItem(
|
||||
@@ -142,6 +151,7 @@ export default defineComponent({
|
||||
deleting,
|
||||
isBatch,
|
||||
adminEnabled,
|
||||
userInviteModalActive,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,6 +142,10 @@ export default defineComponent({
|
||||
permission.value = response.data.data;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
|
||||
if (err?.response?.status === 403) {
|
||||
router.push(`/settings/roles/${props.roleKey || 'public'}`);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,16 @@
|
||||
<v-icon name="edit" outline />
|
||||
</v-button>
|
||||
|
||||
<v-button
|
||||
v-if="canInviteUsers"
|
||||
rounded
|
||||
icon
|
||||
@click="userInviteModalActive = true"
|
||||
v-tooltip.bottom="$t('invite_users')"
|
||||
>
|
||||
<v-icon name="person_add" />
|
||||
</v-button>
|
||||
|
||||
<v-button rounded icon :to="addNewLink" v-tooltip.bottom="$t('create_user')">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
@@ -58,6 +68,8 @@
|
||||
<users-navigation :current-role="queryFilters && queryFilters.role" />
|
||||
</template>
|
||||
|
||||
<users-invite v-if="canInviteUsers" v-model="userInviteModalActive" />
|
||||
|
||||
<component
|
||||
class="layout"
|
||||
ref="layoutRef"
|
||||
@@ -104,6 +116,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, PropType } from '@vue/composition-api';
|
||||
import UsersNavigation from '../components/navigation.vue';
|
||||
import UsersInvite from '@/views/private/components/users-invite';
|
||||
|
||||
import { i18n } from '@/lang';
|
||||
import api from '@/api';
|
||||
@@ -111,6 +124,7 @@ import { LayoutComponent } from '@/layouts/types';
|
||||
import usePreset from '@/composables/use-preset';
|
||||
import LayoutSidebarDetail from '@/views/private/components/layout-sidebar-detail';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
import { useUserStore, usePermissionsStore } from '@/stores';
|
||||
import marked from 'marked';
|
||||
import useNavigation from '../composables/use-navigation';
|
||||
|
||||
@@ -120,7 +134,7 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'users-collection',
|
||||
components: { UsersNavigation, LayoutSidebarDetail, SearchInput },
|
||||
components: { UsersNavigation, LayoutSidebarDetail, SearchInput, UsersInvite },
|
||||
props: {
|
||||
queryFilters: {
|
||||
type: Object as PropType<Record<string, string>>,
|
||||
@@ -130,6 +144,9 @@ export default defineComponent({
|
||||
setup(props) {
|
||||
const { roles } = useNavigation();
|
||||
const layoutRef = ref<LayoutComponent | null>(null);
|
||||
const userInviteModalActive = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const permissionsStore = usePermissionsStore();
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
@@ -157,7 +174,23 @@ export default defineComponent({
|
||||
return filters.value;
|
||||
});
|
||||
|
||||
const canInviteUsers = computed(() => {
|
||||
const isAdmin = !!userStore.state.currentUser?.role?.admin_access;
|
||||
|
||||
if (isAdmin) return true;
|
||||
|
||||
const usersCreatePermission = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.collection === 'directus_users' && permission.action === 'create'
|
||||
);
|
||||
const rolesReadPermission = permissionsStore.state.permissions.find(
|
||||
(permission) => permission.collection === 'directus_roles' && permission.action === 'read'
|
||||
);
|
||||
|
||||
return !!usersCreatePermission && !!rolesReadPermission;
|
||||
});
|
||||
|
||||
return {
|
||||
canInviteUsers,
|
||||
_filters,
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
@@ -175,6 +208,7 @@ export default defineComponent({
|
||||
searchQuery,
|
||||
marked,
|
||||
clearFilters,
|
||||
userInviteModalActive,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<div class="user-box" v-if="isNew === false">
|
||||
<div class="avatar">
|
||||
<v-skeleton-loader v-if="loading || previewLoading" />
|
||||
<img v-else-if="avatarSrc" :src="avatarSrc" :alt="item.first_name" />
|
||||
<img v-else-if="avatarSrc" :src="avatarSrc" :alt="item.email" />
|
||||
<v-icon v-else name="account_circle" outline x-large />
|
||||
</div>
|
||||
<div class="user-box-content">
|
||||
@@ -112,7 +112,7 @@
|
||||
<v-skeleton-loader type="text" />
|
||||
</template>
|
||||
<template v-else-if="isNew === false">
|
||||
<div class="name type-title">{{ item.first_name }} {{ item.last_name }}</div>
|
||||
<div class="name type-title">{{ userName(item) }}</div>
|
||||
<div class="email">{{ item.email }}</div>
|
||||
<v-chip :class="item.status" small>{{ roleName }}</v-chip>
|
||||
</template>
|
||||
@@ -179,6 +179,7 @@ import { getRootPath } from '@/utils/get-root-path';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
import { isAllowed } from '@/utils/is-allowed';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type Values = {
|
||||
[field: string]: any;
|
||||
@@ -255,7 +256,7 @@ export default defineComponent({
|
||||
|
||||
if (isNew.value === false && item.value !== null) {
|
||||
const user = item.value as any;
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
return userName(user);
|
||||
}
|
||||
|
||||
return i18n.t('adding_user');
|
||||
@@ -334,6 +335,7 @@ export default defineComponent({
|
||||
archiving,
|
||||
archiveTooltip,
|
||||
form,
|
||||
userName,
|
||||
};
|
||||
|
||||
function useBreadcrumb() {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { defineComponent, computed, watch, ref } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { hydrate } from '@/hydrate';
|
||||
import router from '@/router';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
@@ -36,11 +37,11 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.get(`/users/me`, {
|
||||
params: {
|
||||
fields: ['first_name', 'last_name', 'last_page'],
|
||||
fields: ['email', 'first_name', 'last_name', 'last_page'],
|
||||
},
|
||||
});
|
||||
|
||||
name.value = response.data.data.first_name + ' ' + response.data.data.last_name;
|
||||
name.value = userName(response.data.data);
|
||||
lastPage.value = response.data.data.last_page;
|
||||
} catch (err) {
|
||||
error.value = err;
|
||||
|
||||
@@ -3,6 +3,7 @@ import api from '@/api';
|
||||
import { useLatencyStore } from '@/stores';
|
||||
|
||||
import { User } from '@/types';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export const useUserStore = createStore({
|
||||
id: 'userStore',
|
||||
@@ -14,7 +15,7 @@ export const useUserStore = createStore({
|
||||
getters: {
|
||||
fullName(state) {
|
||||
if (state.currentUser === null) return null;
|
||||
return state.currentUser.first_name + ' ' + state.currentUser.last_name;
|
||||
return userName(state.currentUser);
|
||||
},
|
||||
isAdmin(state) {
|
||||
return state.currentUser?.role.admin_access === true || false;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.type-label {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
// full by default
|
||||
|
||||
18
app/src/utils/user-name.ts
Normal file
18
app/src/utils/user-name.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { User } from '@/types';
|
||||
import { i18n } from '@/lang';
|
||||
|
||||
export function userName(user: Partial<User>): string {
|
||||
if (user.first_name && user.last_name) {
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
}
|
||||
|
||||
if (user.first_name) {
|
||||
return user.first_name;
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
return user.email;
|
||||
}
|
||||
|
||||
return i18n.t('unknown_user') as string;
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
ref="textarea"
|
||||
>
|
||||
<template #append>
|
||||
<v-icon name="alternate_email" class="add-mention" />
|
||||
<v-icon name="insert_emoticon" class="add-emoji" />
|
||||
<!-- <v-icon name="alternate_email" class="add-mention" />
|
||||
<v-icon name="insert_emoticon" class="add-emoji" /> -->
|
||||
<v-button
|
||||
:disabled="!newCommentContent || newCommentContent.length === 0"
|
||||
:loading="saving"
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<div class="comment-header">
|
||||
<v-avatar x-small>
|
||||
<img
|
||||
v-if="avatarSource"
|
||||
:src="avatarSource"
|
||||
:alt="activity.user.first_name + ' ' + activity.user.last_name"
|
||||
/>
|
||||
<img v-if="avatarSource" :src="avatarSource" :alt="userName(activity.user)" />
|
||||
<v-icon v-else name="person_outline" />
|
||||
</v-avatar>
|
||||
|
||||
@@ -13,7 +9,7 @@
|
||||
<user-popover v-if="activity.user && activity.user.id" :user="activity.user.id">
|
||||
<span>
|
||||
<template v-if="activity.user && activity.user">
|
||||
{{ activity.user.first_name }} {{ activity.user.last_name }}
|
||||
{{ userName(activity.user) }}
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
@@ -28,7 +24,6 @@
|
||||
<template #activator="{ toggle, active }">
|
||||
<v-icon class="more" :class="{ active }" name="more_horiz" @click="toggle" />
|
||||
<div class="time">
|
||||
<span class="dot" v-tooltip="editedOnFormatted" />
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,6 +65,7 @@ import { Activity } from './types';
|
||||
import format from 'date-fns/format';
|
||||
import i18n from '@/lang';
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
import api from '@/api';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
@@ -86,20 +82,6 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const editedOnFormatted = ref('');
|
||||
|
||||
watch(
|
||||
() => props.activity,
|
||||
async () => {
|
||||
if (props.activity.edited_on) {
|
||||
editedOnFormatted.value = await localizedFormat(
|
||||
new Date(props.activity.edited_on),
|
||||
String(i18n.t('date-fns_datetime'))
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
if (props.activity.timestamp) {
|
||||
// timestamp is in iso-8601
|
||||
@@ -117,7 +99,7 @@ export default defineComponent({
|
||||
|
||||
const { confirmDelete, deleting, remove } = useDelete();
|
||||
|
||||
return { formattedTime, avatarSource, confirmDelete, deleting, remove, editedOnFormatted };
|
||||
return { formattedTime, avatarSource, confirmDelete, deleting, remove, userName };
|
||||
|
||||
function useDelete() {
|
||||
const confirmDelete = ref(false);
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
<div class="comment-item">
|
||||
<comment-item-header :refresh="refresh" :activity="activity" @edit="editing = true" />
|
||||
|
||||
<v-textarea v-if="editing" v-model="edits">
|
||||
<v-textarea ref="textarea" v-if="editing" v-model="edits">
|
||||
<template #append>
|
||||
<div class="buttons">
|
||||
<v-button class="cancel" @click="cancelEditing" secondary x-small>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
|
||||
<v-button :loading="savingEdits" class="post-comment" @click="saveEdits" x-small>
|
||||
<v-button
|
||||
:loading="savingEdits"
|
||||
class="post-comment"
|
||||
@click="saveEdits"
|
||||
x-small
|
||||
:disabled="edits === activity.comment"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</v-button>
|
||||
</div>
|
||||
@@ -32,6 +38,7 @@ import { defineComponent, PropType, ref, computed, watch } from '@vue/compositio
|
||||
import { Activity } from './types';
|
||||
import CommentItemHeader from './comment-item-header.vue';
|
||||
import marked from 'marked';
|
||||
import useShortcut from '@/composables/use-shortcut';
|
||||
|
||||
import api from '@/api';
|
||||
|
||||
@@ -48,11 +55,14 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const textarea = ref<Vue>();
|
||||
const htmlContent = computed(() => (props.activity.comment ? marked(props.activity.comment) : null));
|
||||
|
||||
const { edits, editing, savingEdits, saveEdits, cancelEditing } = useEdits();
|
||||
|
||||
return { htmlContent, edits, editing, savingEdits, saveEdits, cancelEditing };
|
||||
useShortcut('meta+enter', saveEdits, textarea);
|
||||
|
||||
return { htmlContent, edits, editing, savingEdits, saveEdits, cancelEditing, textarea };
|
||||
|
||||
function useEdits() {
|
||||
const edits = ref(props.activity.comment);
|
||||
|
||||
@@ -81,6 +81,7 @@ export default defineComponent({
|
||||
'action',
|
||||
'timestamp',
|
||||
'user.id',
|
||||
'user.email',
|
||||
'user.first_name',
|
||||
'user.last_name',
|
||||
'user.avatar.id',
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Avatar } from '@/types';
|
||||
import { User } from '@/types';
|
||||
|
||||
export type Activity = {
|
||||
id: number;
|
||||
action: 'comment';
|
||||
user: null | {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
avatar: null | Avatar;
|
||||
};
|
||||
user: null | Partial<User>;
|
||||
timestamp: string;
|
||||
edited_on: null | string;
|
||||
comment: null | string;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { defineComponent, PropType, computed } from '@vue/composition-api';
|
||||
import { Revision } from './types';
|
||||
import i18n from '@/lang';
|
||||
import { format } from 'date-fns';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -61,11 +62,7 @@ export default defineComponent({
|
||||
|
||||
const user = computed(() => {
|
||||
if (props.revision?.activity?.user && typeof props.revision.activity.user === 'object') {
|
||||
const { first_name, last_name } = props.revision.activity.user as {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
return `${first_name} ${last_name}`;
|
||||
return userName(props.revision.activity.user);
|
||||
}
|
||||
|
||||
return i18n.t('private_user');
|
||||
|
||||
@@ -131,6 +131,7 @@ export default defineComponent({
|
||||
'activity.action',
|
||||
'activity.timestamp',
|
||||
'activity.user.id',
|
||||
'activity.user.email',
|
||||
'activity.user.first_name',
|
||||
'activity.user.last_name',
|
||||
'activity.ip',
|
||||
@@ -191,11 +192,6 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar-detail {
|
||||
--v-badge-color: var(--background-normal);
|
||||
--v-badge-background-color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.v-progress-linear {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Revision } from './types';
|
||||
import useSync from '@/composables/use-sync';
|
||||
import localizedFormat from '@/utils/localized-format';
|
||||
import i18n from '@/lang';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type Option = {
|
||||
text: string;
|
||||
@@ -59,8 +60,8 @@ export default defineComponent({
|
||||
let user = i18n.t('private_user');
|
||||
|
||||
if (typeof revision.activity.user === 'object') {
|
||||
const { first_name, last_name } = revision.activity.user;
|
||||
user = `${first_name} ${last_name}`;
|
||||
const userInfo = revision.activity.user;
|
||||
user = userName(userInfo);
|
||||
}
|
||||
|
||||
const text = String(i18n.t('revision_delta_by', { date, user }));
|
||||
|
||||
@@ -10,9 +10,10 @@ export type Revision = {
|
||||
user_agent: string;
|
||||
timestamp: string;
|
||||
user:
|
||||
| number
|
||||
| string
|
||||
| {
|
||||
id: number;
|
||||
id: string;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ body {
|
||||
--v-badge-offset-x: 2px;
|
||||
--v-badge-offset-y: 4px;
|
||||
--v-badge-border-color: var(--background-normal-alt);
|
||||
--v-badge-background-color: var(--foreground-normal);
|
||||
--v-badge-color: var(--background-normal);
|
||||
|
||||
display: contents;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<v-icon name="person" outline v-else />
|
||||
</v-avatar>
|
||||
<div class="data">
|
||||
<div class="name type-title">{{ data.first_name }} {{ data.last_name }}</div>
|
||||
<div class="name type-title">{{ userName(data) }}</div>
|
||||
<div class="status-role" :class="data.status">{{ $t(data.status) }} {{ data.role.name }}</div>
|
||||
<div class="email">{{ data.email }}</div>
|
||||
</div>
|
||||
@@ -33,6 +33,7 @@
|
||||
import { defineComponent, ref, watch, onUnmounted, computed } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { getRootPath } from '@/utils/get-root-path';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
type User = {
|
||||
first_name: string;
|
||||
@@ -77,7 +78,7 @@ export default defineComponent({
|
||||
data.value = null;
|
||||
});
|
||||
|
||||
return { loading, error, data, active, avatarSrc };
|
||||
return { loading, error, data, active, avatarSrc, userName };
|
||||
|
||||
async function fetchUser() {
|
||||
loading.value = true;
|
||||
@@ -86,7 +87,7 @@ export default defineComponent({
|
||||
try {
|
||||
const response = await api.get(`/users/${props.user}`, {
|
||||
params: {
|
||||
fields: ['first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
|
||||
fields: ['email', 'first_name', 'last_name', 'avatar.id', 'role.name', 'status', 'email'],
|
||||
},
|
||||
});
|
||||
data.value = response.data.data;
|
||||
|
||||
4
app/src/views/private/components/users-invite/index.ts
Normal file
4
app/src/views/private/components/users-invite/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import UsersInvite from './users-invite.vue';
|
||||
|
||||
export { UsersInvite };
|
||||
export default UsersInvite;
|
||||
121
app/src/views/private/components/users-invite/users-invite.vue
Normal file
121
app/src/views/private/components/users-invite/users-invite.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<v-dialog :active="active" @toggle="$emit('toggle', $event)" @esc="$emit('toggle', false)">
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('invite_users') }}</v-card-title>
|
||||
|
||||
<v-card-text class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('emails') }}</div>
|
||||
<interface-tags
|
||||
v-model="emails"
|
||||
:placeholder="$t('email_examples')"
|
||||
icon-right="email"
|
||||
whitespace=""
|
||||
/>
|
||||
</div>
|
||||
<div class="field" v-if="role === null">
|
||||
<div class="type-label">{{ $t('role') }}</div>
|
||||
<v-select v-model="roleSelected" :items="roles" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button secondary @click="$emit('toggle', false)">{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="inviteUsers" :disabled="emails === null || emails.length === 0" :loading="loading">
|
||||
{{ $t('invite') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, PropType, watch } from '@vue/composition-api';
|
||||
import api from '@/api';
|
||||
import { useNotificationsStore } from '@/stores';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default defineComponent({
|
||||
model: {
|
||||
prop: 'active',
|
||||
event: 'toggle',
|
||||
},
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const notifications = useNotificationsStore();
|
||||
const emails = ref<string[]>([]);
|
||||
const roles = ref<Record<string, any>[]>([]);
|
||||
const roleSelected = ref<string | null>(props.role);
|
||||
const loading = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.active,
|
||||
() => {
|
||||
loadRoles();
|
||||
}
|
||||
);
|
||||
|
||||
return { emails, inviteUsers, roles, roleSelected, loading };
|
||||
|
||||
async function inviteUsers() {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all(
|
||||
emails.value.map((email) => {
|
||||
return api.post('/users/invite', {
|
||||
email,
|
||||
role: roleSelected.value,
|
||||
});
|
||||
})
|
||||
);
|
||||
emit('toggle', false);
|
||||
} catch (err) {
|
||||
notifications.add({
|
||||
title: i18n.t('server_error'),
|
||||
text: err.message,
|
||||
persist: true,
|
||||
type: 'error',
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
const response = await api.get('/roles');
|
||||
|
||||
roles.value = response.data.data.map((role: Record<string, any>) => ({
|
||||
text: role.name,
|
||||
value: role.id,
|
||||
}));
|
||||
|
||||
if (roles.value.length > 0 && roleSelected.value === null) {
|
||||
roleSelected.value = roles.value[0].value;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.v-card-text {
|
||||
--v-form-vertical-gap: 20px;
|
||||
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
- name: Getting Started
|
||||
icon: play_arrow
|
||||
icon: play_circle_outline
|
||||
to: "/getting-started"
|
||||
children:
|
||||
- name: Introduction
|
||||
|
||||
2
docs/package-lock.json
generated
2
docs/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/docs",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@directus/docs",
|
||||
"private": false,
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"docs",
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"command": {
|
||||
"bootstrap": {
|
||||
"npmClientArgs": [
|
||||
|
||||
141
package-lock.json
generated
141
package-lock.json
generated
@@ -8216,13 +8216,6 @@
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
@@ -8260,83 +8253,6 @@
|
||||
"worker-rpc": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
|
||||
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"globby": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz",
|
||||
@@ -8382,18 +8298,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
@@ -18575,6 +18479,51 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fork-ts-checker-webpack-plugin-v5": {
|
||||
"version": "npm:fork-ts-checker-webpack-plugin@5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-5.2.0.tgz",
|
||||
"integrity": "sha512-NEKcI0+osT5bBFZ1SFGzJMQETjQWZrSvMO1g0nAR/w0t328Z41eN8BJEIZyFCl2HsuiJpa9AN474Nh2qLVwGLQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@types/json-schema": "^7.0.5",
|
||||
"chalk": "^4.1.0",
|
||||
"cosmiconfig": "^6.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"fs-extra": "^9.0.0",
|
||||
"memfs": "^3.1.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"schema-utils": "2.7.0",
|
||||
"semver": "^7.3.2",
|
||||
"tapable": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"schema-utils": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz",
|
||||
"integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.4",
|
||||
"ajv": "^6.12.2",
|
||||
"ajv-keywords": "^3.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-directus-project",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"description": "A small installer util that will create a directory, add boilerplate folders, and install Directus through npm.",
|
||||
"main": "lib/index.js",
|
||||
"bin": "./lib/index.js",
|
||||
|
||||
2
packages/format-title/package-lock.json
generated
2
packages/format-title/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/format-title",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/format-title",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"description": "Custom string formatter that converts any string into [Title Case](http://www.grammar-monster.com/lessons/capital_letters_title_case.htm)",
|
||||
"keywords": [
|
||||
"title-case",
|
||||
|
||||
2
packages/spec/package-lock.json
generated
2
packages/spec/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@directus/specs",
|
||||
"version": "9.0.0-rc.0",
|
||||
"version": "9.0.0-rc.1",
|
||||
"description": "OpenAPI Specification of the Directus API",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -17,29 +17,34 @@ get:
|
||||
description: The key of the asset size configured in settings.
|
||||
schema:
|
||||
type: string
|
||||
- name: w
|
||||
- name: width
|
||||
in: query
|
||||
description: Width of the file in pixels.
|
||||
schema:
|
||||
type: integer
|
||||
- name: h
|
||||
- name: height
|
||||
in: query
|
||||
description: Height of the file in pixels.
|
||||
schema:
|
||||
type: integer
|
||||
- name: f
|
||||
- name: fit
|
||||
in: query
|
||||
description: Fit of the file
|
||||
schema:
|
||||
type: string
|
||||
enum: [crop, contain]
|
||||
- name: q
|
||||
- name: quality
|
||||
in: query
|
||||
description: Quality of compression.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
- name: download
|
||||
in: query
|
||||
description: Download the asset to your computer
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Successful request
|
||||
|
||||
Reference in New Issue
Block a user