Merge pull request #1 from directus/main

pull
This commit is contained in:
Adrian Dimitrov
2020-10-26 22:59:18 +02:00
committed by GitHub
42 changed files with 656 additions and 355 deletions

View File

@@ -12,6 +12,7 @@ import url from 'url';
import path from 'path';
import useCollection from '../middleware/use-collection';
import { respond } from '../middleware/respond';
import { toArray } from '../utils/to-array';
const router = express.Router();
@@ -32,7 +33,7 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
* the row in directus_files async during the upload of the actual file.
*/
let disk: string = (env.STORAGE_LOCATIONS as string).split(',')[0].trim();
let disk: string = toArray(env.STORAGE_LOCATIONS)[0];
let payload: Partial<File> = {};
let fileCount = 0;
@@ -155,7 +156,7 @@ router.post(
const payload = {
filename_download: filename,
storage: (env.STORAGE_LOCATIONS as string).split(',')[0].trim(),
storage: toArray(env.STORAGE_LOCATIONS)[0],
type: fileResponse.headers['content-type'],
title: formatTitle(filename),
...(req.body.data || {}),

View File

@@ -20,12 +20,12 @@ router.get('/ping', (req, res) => res.send('pong'));
router.get(
'/info',
(req, res, next) => {
asyncHandler(async (req, res, next) => {
const service = new ServerService({ accountability: req.accountability });
const data = service.serverInfo();
const data = await service.serverInfo();
res.locals.payload = { data };
return next();
},
}),
respond
);

View File

@@ -1,17 +0,0 @@
table: directus_permissions
defaults:
role: null
collection: null
action: null
permissions: null
validation: null
presets: null
fields: null
limit: null
data:
- collection: directus_settings
action: read
permissions: {}
fields: 'project_name,project_logo,project_color,public_foreground,public_background,public_note,custom_css'

View File

@@ -63,11 +63,6 @@ function processValues(env: Record<string, any>) {
if (value === 'false') env[key] = false;
if (value === 'null') env[key] = null;
if (isNaN(value) === false && value.length > 0) env[key] = Number(value);
if (typeof value === 'string' && value.includes(','))
env[key] = value
.split(',')
.map((val) => val.trim())
.filter((val) => val);
}
return env;

View File

@@ -60,18 +60,28 @@ export async function sendInviteMail(email: string, url: string) {
/**
* @TODO pull this from directus_settings
*/
const projectName = 'directus';
const projectName = 'Directus';
const html = await liquidEngine.renderFile('user-invitation', { email, url, projectName });
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
await transporter.sendMail({
from: env.EMAIL_FROM,
to: email,
html: html,
subject: `[${projectName}] You've been invited`,
});
}
export async function sendPasswordResetMail(email: string, url: string) {
/**
* @TODO pull this from directus_settings
*/
const projectName = 'directus';
const projectName = 'Directus';
const html = await liquidEngine.renderFile('password-reset', { email, url, projectName });
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
await transporter.sendMail({
from: env.EMAIL_FROM,
to: email,
html: html,
subject: `[${projectName}] Password Reset Request`,
});
}

View File

@@ -17,22 +17,25 @@ import { ForbiddenException, FailedValidationException } from '../exceptions';
import { uniq, merge, flatten } from 'lodash';
import generateJoi from '../utils/generate-joi';
import { ItemsService } from './items';
import { PayloadService } from './payload';
import { parseFilter } from '../utils/parse-filter';
import { toArray } from '../utils/to-array';
export class AuthorizationService {
knex: Knex;
accountability: Accountability | null;
payloadService: PayloadService;
constructor(options?: AbstractServiceOptions) {
this.knex = options?.knex || database;
this.accountability = options?.accountability || null;
this.payloadService = new PayloadService('directus_permissions', { knex: this.knex });
}
async processAST(ast: AST, action: PermissionsAction = 'read'): Promise<AST> {
const collectionsRequested = getCollectionsFromAST(ast);
const permissionsForCollections = await this.knex
let permissionsForCollections = await this.knex
.select<Permission[]>('*')
.from('directus_permissions')
.where({ action, role: this.accountability?.role })
@@ -41,6 +44,11 @@ export class AuthorizationService {
collectionsRequested.map(({ collection }) => collection)
);
permissionsForCollections = (await this.payloadService.processValues(
'read',
permissionsForCollections
)) as Permission[];
// If the permissions don't match the collections, you don't have permission to read all of them
const uniqueCollectionsRequestedCount = uniq(
collectionsRequested.map(({ collection }) => collection)
@@ -111,7 +119,7 @@ export class AuthorizationService {
(permission) => permission.collection === collection
)!;
const allowedFields = permissions.fields?.split(',') || [];
const allowedFields = permissions.fields || [];
for (const childNode of ast.children) {
if (childNode.type !== 'field') {
@@ -213,21 +221,26 @@ export class AuthorizationService {
permissions: {},
validation: {},
limit: null,
fields: '*',
fields: ['*'],
presets: {},
};
} else {
permission = await this.knex
.select<Permission>('*')
.select('*')
.from('directus_permissions')
.where({ action, collection, role: this.accountability?.role || null })
.first();
permission = (await this.payloadService.processValues(
'read',
permission as Item
)) as Permission;
// Check if you have permission to access the fields you're trying to acces
if (!permission) throw new ForbiddenException();
const allowedFields = permission.fields?.split(',') || [];
const allowedFields = permission.fields || [];
if (allowedFields.includes('*') === false) {
for (const payload of payloads) {
@@ -260,13 +273,16 @@ export class AuthorizationService {
.from('directus_fields')
.where({ collection, field: column.name })
.first();
const specials = (field?.special || '').split(',');
const hasGenerateSpecial = [
'uuid',
'date-created',
'role-created',
'user-created',
].some((name) => specials.includes(name));
const isRequired =
column.is_nullable === false &&
column.has_auto_increment === false &&

View File

@@ -18,7 +18,6 @@ import {
GraphQLInputObjectType,
ObjectFieldNode,
GraphQLID,
ValueNode,
FieldNode,
GraphQLFieldConfigMap,
GraphQLInt,
@@ -26,11 +25,9 @@ import {
StringValueNode,
BooleanValueNode,
ArgumentNode,
GraphQLScalarType,
GraphQLBoolean,
ObjectValueNode,
GraphQLUnionType,
GraphQLUnionTypeConfig,
} from 'graphql';
import { getGraphQLType } from '../utils/get-graphql-type';
import { RelationsService } from './relations';
@@ -65,7 +62,7 @@ export class GraphQLService {
this.knex = options?.knex || database;
this.fieldsService = new FieldsService(options);
this.collectionsService = new CollectionsService(options);
this.relationsService = new RelationsService({ knex: this.knex });
this.relationsService = new RelationsService(options);
}
args = {
@@ -138,6 +135,7 @@ export class GraphQLService {
const relatedIsSystem = relationForField.one_collection!.startsWith(
'directus_'
);
const relatedType = relatedIsSystem
? schema[relationForField.one_collection!.substring(9)].type
: schema.items[relationForField.one_collection!].type;

View File

@@ -40,7 +40,7 @@ export class MetaService {
const dbQuery = database(collection).count('*', { as: 'count' });
if (query.filter) {
applyFilter(dbQuery, query.filter, collection);
await applyFilter(dbQuery, query.filter, collection);
}
const records = await dbQuery;

View File

@@ -25,7 +25,9 @@ export class RelationsService extends ItemsService {
| ParsedRelation
| ParsedRelation[]
| null;
const filteredResults = await this.filterForbidden(results);
return filteredResults;
}
@@ -58,6 +60,7 @@ export class RelationsService extends ItemsService {
this.accountability?.role || null,
'read'
);
const allowedFields = await this.permissionsService.getAllowedFields(
this.accountability?.role || null,
'read'

View File

@@ -2,7 +2,6 @@ import { AbstractServiceOptions, Accountability } from '../types';
import Knex from 'knex';
import database from '../database';
import os from 'os';
import { ForbiddenException } from '../exceptions';
// @ts-ignore
import { version } from '../../package.json';
import macosRelease from 'macos-release';
@@ -16,31 +15,57 @@ export class ServerService {
this.accountability = options?.accountability || null;
}
serverInfo() {
if (this.accountability?.admin !== true) {
throw new ForbiddenException();
}
async serverInfo() {
const info: Record<string, any> = {};
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
const osVersion =
osType === 'macOS'
? `${macosRelease().name} (${macosRelease().version})`
: os.release();
const projectInfo = await this.knex
.select(
'project_name',
'project_logo',
'project_color',
'public_foreground',
'public_background',
'public_note',
'custom_css'
)
.from('directus_settings')
.first();
return {
directus: {
info.project = projectInfo
? {
project_name: projectInfo.project_name,
project_logo: projectInfo.project_logo,
project_color: projectInfo.project_color,
public_foreground: projectInfo.public_foreground,
public_background: projectInfo.public_background,
public_note: projectInfo.public_note,
custom_css: projectInfo.custom_css,
}
: null;
if (this.accountability?.admin === true) {
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
const osVersion =
osType === 'macOS'
? `${macosRelease().name} (${macosRelease().version})`
: os.release();
info.directus = {
version,
},
node: {
};
info.node = {
version: process.versions.node,
uptime: Math.round(process.uptime()),
},
os: {
};
info.os = {
type: osType,
version: osVersion,
uptime: Math.round(os.uptime()),
totalmem: os.totalmem(),
},
};
};
}
return info;
}
}

View File

@@ -9,5 +9,5 @@ export type Permission = {
validation: Record<string, any>;
limit: number | null;
presets: Record<string, any> | null;
fields: string | null;
fields: string[] | null;
};

View File

@@ -1,7 +1,7 @@
import { QueryBuilder } from 'knex';
import { Query, Filter } from '../types';
import database, { schemaInspector } from '../database';
import { clone } from 'lodash';
import { clone, isPlainObject } from 'lodash';
export default async function applyQuery(collection: string, dbQuery: QueryBuilder, query: Query) {
if (query.filter) {
@@ -46,116 +46,244 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild
}
}
export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
value.forEach((subFilter: Record<string, any>) => {
dbQuery.orWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
});
export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) {
const relations = await database.select('*').from('directus_relations');
continue;
addWhereClauses(rootQuery, rootFilter, collection);
addJoins(rootQuery, rootFilter, collection);
function addWhereClauses(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
/** @NOTE these callback functions aren't called until Knex runs the query */
dbQuery.orWhere((subQuery) => {
value.forEach((subFilter: Record<string, any>) => {
addWhereClauses(subQuery, subFilter, collection);
});
});
continue;
}
if (key === '_and') {
/** @NOTE these callback functions aren't called until Knex runs the query */
dbQuery.andWhere((subQuery) => {
value.forEach((subFilter: Record<string, any>) => {
addWhereClauses(subQuery, subFilter, collection);
});
});
continue;
}
const filterPath = getFilterPath(key, value);
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
if (filterPath.length > 1) {
const columnName = getWhereColumn(filterPath, collection);
applyFilterToQuery(columnName, filterOperator, filterValue);
} else {
applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue);
}
}
if (key === '_and') {
value.forEach((subFilter: Record<string, any>) => {
dbQuery.andWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
});
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
if (operator === '_eq') {
dbQuery.where({ [key]: compareValue });
}
continue;
if (operator === '_neq') {
dbQuery.whereNot({ [key]: compareValue });
}
if (operator === '_contains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
if (operator === '_ncontains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
if (operator === '_gt') {
dbQuery.where(key, '>', compareValue);
}
if (operator === '_gte') {
dbQuery.where(key, '>=', compareValue);
}
if (operator === '_lt') {
dbQuery.where(key, '<', compareValue);
}
if (operator === '_lte') {
dbQuery.where(key, '<=', compareValue);
}
if (operator === '_in') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereIn(key, value as string[]);
}
if (operator === '_nin') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotIn(key, value as string[]);
}
if (operator === '_null') {
dbQuery.whereNull(key);
}
if (operator === '_nnull') {
dbQuery.whereNotNull(key);
}
if (operator === '_empty') {
dbQuery.andWhere((query) => {
query.whereNull(key);
query.orWhere(key, '=', '');
});
}
if (operator === '_nempty') {
dbQuery.andWhere((query) => {
query.whereNotNull(key);
query.orWhere(key, '!=', '');
});
}
if (operator === '_between') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereBetween(key, value);
}
if (operator === '_nbetween') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotBetween(key, value);
}
}
const filterPath = getFilterPath(key, value);
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
function getWhereColumn(path: string[], collection: string) {
path = clone(path);
const column =
filterPath.length > 1
? await applyJoins(dbQuery, filterPath, collection)
: `${collection}.${filterPath[0]}`;
let columnName = '';
applyFilterToQuery(column, filterOperator, filterValue);
followRelation(path);
return columnName;
function followRelation(pathParts: string[], parentCollection: string = collection) {
const relation = relations.find((relation) => {
return (
(relation.many_collection === parentCollection &&
relation.many_field === pathParts[0]) ||
(relation.one_collection === parentCollection &&
relation.one_field === pathParts[0])
);
});
if (!relation) return;
const isM2O =
relation.many_collection === parentCollection &&
relation.many_field === pathParts[0];
pathParts.shift();
const parent = isM2O ? relation.one_collection! : relation.many_collection;
if (pathParts.length === 1) {
columnName = `${parent}.${pathParts[0]}`;
}
if (pathParts.length) {
followRelation(pathParts, parent);
}
}
}
}
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
if (operator === '_eq') {
dbQuery.where({ [key]: compareValue });
/**
* @NOTE Yes this is very similar in structure and functionality as the other loop. However,
* due to the order of execution that Knex has in the nested andWhere / orWhere structures,
* joins that are added in there aren't added in time
*/
function addJoins(dbQuery: QueryBuilder, filter: Filter, collection: string) {
for (const [key, value] of Object.entries(filter)) {
if (key === '_or') {
value.forEach((subFilter: Record<string, any>) => {
addJoins(dbQuery, subFilter, collection);
});
continue;
}
if (key === '_and') {
value.forEach((subFilter: Record<string, any>) => {
addJoins(dbQuery, subFilter, collection);
});
continue;
}
const filterPath = getFilterPath(key, value);
if (filterPath.length > 1) {
addJoin(filterPath, collection);
}
}
if (operator === '_neq') {
dbQuery.whereNot({ [key]: compareValue });
}
function addJoin(path: string[], collection: string) {
path = clone(path);
if (operator === '_contains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
followRelation(path);
if (operator === '_ncontains') {
dbQuery.where(key, 'like', `%${compareValue}%`);
}
function followRelation(pathParts: string[], parentCollection: string = collection) {
const relation = relations.find((relation) => {
return (
(relation.many_collection === parentCollection &&
relation.many_field === pathParts[0]) ||
(relation.one_collection === parentCollection &&
relation.one_field === pathParts[0])
);
});
if (operator === '_gt') {
dbQuery.where(key, '>', compareValue);
}
if (!relation) return;
if (operator === '_gte') {
dbQuery.where(key, '>=', compareValue);
}
const isM2O =
relation.many_collection === parentCollection &&
relation.many_field === pathParts[0];
if (operator === '_lt') {
dbQuery.where(key, '<', compareValue);
}
if (isM2O) {
dbQuery.leftJoin(
relation.one_collection!,
`${parentCollection}.${relation.many_field}`,
`${relation.one_collection}.${relation.one_primary}`
);
} else {
dbQuery.leftJoin(
relation.many_collection,
`${parentCollection}.${relation.one_primary}`,
`${relation.many_collection}.${relation.many_field}`
);
}
if (operator === '_lte') {
dbQuery.where(key, '<=', compareValue);
}
pathParts.shift();
if (operator === '_in') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
const parent = isM2O ? relation.one_collection! : relation.many_collection;
dbQuery.whereIn(key, value as string[]);
}
if (operator === '_nin') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotIn(key, value as string[]);
}
if (operator === '_null') {
dbQuery.whereNull(key);
}
if (operator === '_nnull') {
dbQuery.whereNotNull(key);
}
if (operator === '_empty') {
dbQuery.andWhere((query) => {
query.whereNull(key);
query.orWhere(key, '=', '');
});
}
if (operator === '_nempty') {
dbQuery.andWhere((query) => {
query.whereNotNull(key);
query.orWhere(key, '!=', '');
});
}
if (operator === '_between') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereBetween(key, value);
}
if (operator === '_nbetween') {
let value = compareValue;
if (typeof value === 'string') value = value.split(',');
dbQuery.whereNotBetween(key, value);
if (pathParts.length) {
followRelation(pathParts, parent);
}
}
}
}
}
@@ -163,7 +291,11 @@ export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collect
function getFilterPath(key: string, value: Record<string, any>) {
const path = [key];
if (Object.keys(value)[0].startsWith('_') === false) {
if (Object.keys(value)[0].startsWith('_') === true) {
return path;
}
if (isPlainObject(value)) {
path.push(...getFilterPath(Object.keys(value)[0], Object.values(value)[0]));
}
@@ -171,57 +303,11 @@ function getFilterPath(key: string, value: Record<string, any>) {
}
function getOperation(key: string, value: Record<string, any>): { operator: string; value: any } {
if (key.startsWith('_') && key !== '_and' && key !== '_or')
if (key.startsWith('_') && key !== '_and' && key !== '_or') {
return { operator: key as string, value };
} else if (isPlainObject(value) === false) {
return { operator: '_eq', value };
}
return getOperation(Object.keys(value)[0], Object.values(value)[0]);
}
async function applyJoins(dbQuery: QueryBuilder, path: string[], collection: string) {
path = clone(path);
let keyName = '';
await addJoins(path);
return keyName;
async function addJoins(pathParts: string[], parentCollection: string = collection) {
const relation = await database
.select('*')
.from('directus_relations')
.where({ one_collection: parentCollection, one_field: pathParts[0] })
.orWhere({ many_collection: parentCollection, many_field: pathParts[0] })
.first();
if (!relation) return;
const isM2O =
relation.many_collection === parentCollection && relation.many_field === pathParts[0];
if (isM2O) {
dbQuery.leftJoin(
relation.one_collection,
`${parentCollection}.${relation.many_field}`,
`${relation.one_collection}.${relation.one_primary}`
);
} else {
dbQuery.leftJoin(
relation.many_collection,
`${relation.one_collection}.${relation.one_primary}`,
`${relation.many_collection}.${relation.many_field}`
);
}
pathParts.shift();
const parent = isM2O ? relation.one_collection : relation.many_collection;
if (pathParts.length === 1) {
keyName = `${parent}.${pathParts[0]}`;
}
if (pathParts.length) {
await addJoins(pathParts, parent);
}
}
}

View File

@@ -1,3 +1,7 @@
export function toArray<T = any>(val: T | T[]): T[] {
if (typeof val === 'string') {
return (val.split(',') as unknown) as T[];
}
return Array.isArray(val) ? val : [val];
}

162
app/package-lock.json generated
View File

@@ -6601,51 +6601,6 @@
"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": {
@@ -6785,17 +6740,6 @@
"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",
@@ -6879,18 +6823,6 @@
"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",
@@ -7004,18 +6936,6 @@
"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",
@@ -11744,6 +11664,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",
@@ -20377,6 +20342,43 @@
}
}
},
"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",

View File

@@ -34,25 +34,25 @@ import setFavicon from '@/utils/set-favicon';
export default defineComponent({
setup() {
const { useAppStore, useUserStore, useSettingsStore } = stores;
const { useAppStore, useUserStore, useServerStore } = stores;
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
const serverStore = useServerStore();
const { hydrating, sidebarOpen } = toRefs(appStore.state);
const brandStyle = computed(() => {
return {
'--brand': settingsStore.state.settings?.project_color || 'var(--primary)',
'--brand': serverStore.state.info?.project?.project_color || 'var(--primary)',
};
});
watch(
[() => settingsStore.state.settings?.project_color, () => settingsStore.state.settings?.project_logo],
[() => serverStore.state.info?.project?.project_color, () => serverStore.state.info?.project?.project_logo],
() => {
const hasCustomLogo = !!settingsStore.state.settings?.project_logo;
setFavicon(settingsStore.state.settings?.project_color || '#2f80ed', hasCustomLogo);
const hasCustomLogo = !!serverStore.state.info?.project?.project_logo;
setFavicon(serverStore.state.info?.project?.project_color || '#2f80ed', hasCustomLogo);
}
);
@@ -90,14 +90,14 @@ export default defineComponent({
);
watch(
() => settingsStore.state.settings?.project_name,
() => serverStore.state.info?.project?.project_name,
(projectName) => {
document.title = projectName;
document.title = projectName || 'Directus';
}
);
const customCSS = computed(() => {
return settingsStore.state?.settings?.custom_css || '';
return serverStore.state?.info?.project?.custom_css || '';
});
const error = computed(() => appStore.state.error);

View File

@@ -16,6 +16,20 @@ export function useCollection(collectionKey: string | Ref<string>) {
return fieldsStore.getFieldsForCollection(collection.value);
});
const defaults = computed(() => {
if (!fields.value) return {};
const defaults: Record<string, any> = {};
for (const field of fields.value) {
if (field.schema?.default_value) {
defaults[field.field] = field.schema.default_value;
}
}
return defaults;
});
const primaryKeyField = computed(() => {
// Every collection has a primary key; rules of the land
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -32,5 +46,5 @@ export function useCollection(collectionKey: string | Ref<string>) {
return info.value?.meta?.sort_field || null;
});
return { info, fields, primaryKeyField, userCreatedField, sortField };
return { info, fields, defaults, primaryKeyField, userCreatedField, sortField };
}

View File

@@ -16,7 +16,7 @@ export function useItem(collection: Ref<string>, primaryKey: Ref<string | number
const saving = ref(false);
const deleting = ref(false);
const archiving = ref(false);
const edits = ref({});
const edits = ref<Record<string, any>>({});
const isNew = computed(() => primaryKey.value === '+');
const isBatch = computed(() => typeof primaryKey.value === 'string' && primaryKey.value.includes(','));
const isSingle = computed(() => !!collectionInfo.value?.meta?.singleton);

View File

@@ -6,6 +6,7 @@ import {
useRequestsStore,
usePresetsStore,
useSettingsStore,
useServerStore,
useLatencyStore,
useRelationsStore,
usePermissionsStore,
@@ -30,6 +31,7 @@ export function useStores(
useRequestsStore,
usePresetsStore,
useSettingsStore,
useServerStore,
useLatencyStore,
useRelationsStore,
usePermissionsStore,

View File

@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
icon: 'note_add',
component: InterfaceFile,
types: ['uuid'],
localTypes: ['file'],
relationship: 'm2o',
options: [],
recommendedDisplays: ['file'],

View File

@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
icon: 'note_add',
component: InterfaceFiles,
types: ['alias'],
localTypes: ['files'],
relationship: 'm2m',
options: [],
recommendedDisplays: ['files'],

View File

@@ -8,6 +8,7 @@ export default defineInterface(({ i18n }) => ({
icon: 'insert_photo',
component: InterfaceImage,
types: ['uuid'],
localTypes: ['file'],
relationship: 'm2o',
options: [],
recommendedDisplays: ['image'],

View File

@@ -10,6 +10,7 @@ export default defineInterface(({ i18n }) => ({
component: InterfaceManyToMany,
relationship: 'm2m',
types: ['alias'],
localTypes: ['m2m'],
options: Options,
recommendedDisplays: ['related-values'],
}));

View File

@@ -10,6 +10,7 @@ export default defineInterface(({ i18n }) => ({
component: InterfaceManyToOne,
types: ['uuid', 'string', 'text', 'integer', 'bigInteger'],
relationship: 'm2o',
localTypes: ['m2o'],
options: Options,
recommendedDisplays: ['related-values'],
}));

View File

@@ -9,6 +9,7 @@ export default defineInterface(({ i18n }) => ({
icon: 'arrow_right_alt',
component: InterfaceOneToMany,
types: ['alias'],
localTypes: ['o2m'],
relationship: 'o2m',
options: Options,
recommendedDisplays: ['related-values'],

View File

@@ -1,6 +1,6 @@
import VueI18n from 'vue-i18n';
import { Component } from 'vue';
import { Field, types } from '@/types';
import { Field, types, localTypes } from '@/types';
export type InterfaceConfig = {
id: string;
@@ -10,6 +10,7 @@ export type InterfaceConfig = {
component: Component;
options: DeepPartial<Field>[] | Component;
types: typeof types[number][];
localTypes?: readonly typeof localTypes[number][];
relationship?: null | 'm2o' | 'o2m' | 'm2m' | 'translations';
hideLabel?: boolean;
hideLoader?: boolean;

View File

@@ -61,7 +61,7 @@
@click="setCurrent(item)"
>
<v-list-item-content>
{{ userName(currentUser) }}
{{ userName(item) }}
</v-list-item-content>
</v-list-item>
</template>
@@ -93,6 +93,7 @@ import useCollection from '@/composables/use-collection';
import api from '@/api';
import DrawerItem from '@/views/private/components/drawer-item';
import DrawerCollection from '@/views/private/components/drawer-collection';
import { userName } from '@/utils/user-name';
export default defineComponent({
components: { DrawerItem, DrawerCollection },
@@ -141,6 +142,7 @@ export default defineComponent({
edits,
stageEdits,
editModalActive,
userName,
};
function useCurrent() {

View File

@@ -9,7 +9,17 @@
color: var(--foreground-normal);
}
.tox .tox-tbtn svg {
.tox .tox-listbox__select-chevron svg,
.tox .tox-collection__item-caret svg {
fill: var(--foreground-normal);
}
.tox .tox-swatches__picker-btn svg {
fill: var(--foreground-normal);
}
.tox .tox-tbtn svg,
.tox .tox-tbtn:hover svg {
fill: var(--foreground-normal);
}
@@ -97,6 +107,10 @@
left 0 top 0 var(--background-subdued);
}
.tox .tox-pop__dialog .tox-toolbar {
margin-bottom: -2px;
}
body.dark .tox .tox-toolbar,
body.dark .tox .tox-toolbar__primary,
body.dark .tox .tox-toolbar__overflow {
@@ -113,13 +127,34 @@ body.dark .tox .tox-toolbar__overflow {
}
}
.tox .tox-swatches__picker-btn,
.tox .tox-swatches__picker-btn:hover,
.tox .tox-swatches__picker-btn:active,
.tox .tox-split-button:hover {
-webkit-box-shadow: unset;
box-shadow: unset;
}
.tox .tox-tbtn--enabled,
.tox .tox-tbtn--enabled:hover,
.tox .tox-tbtn:hover {
.tox .tox-split-button:hover,
.tox .tox-tbtn:hover,
.tox .tox-split-button:focus {
color: var(--foreground-normal);
background: var(--border-normal);
}
.tox .tox-swatches__picker-btn:hover {
background: transparent;
border: none;
}
.tox .tox-swatch:hover,
.tox .tox-swatch:focus {
-webkit-transform: scale(1.2);
transform: scale(1.2);
}
.mce-content-body {
margin: 20px;
}
@@ -206,20 +241,16 @@ body.dark .tox .tox-toolbar__overflow {
color: var(--foreground-normal);
}
.tox .tox-collection--list .tox-collection__item--enabled,
.tox .tox-collection--list .tox-collection__item--active {
color: var(--foreground-normal) !important;
background-color: var(--background-page) !important;
}
.tox .tox-collection--list .tox-collection__item--enabled {
color: var(--foreground-normal);
background-color: var(--background-page);
background-color: var(--background-normal-alt) !important;
}
.tox .tox-textfield:focus,
.tox .tox-selectfield select:focus,
.tox .tox-textarea:focus {
border-color: var(--foreground-subdued);
border-color: var(--primary);
}
.tox .tox-button {
@@ -314,8 +345,39 @@ body.dark .tox .tox-toolbar__overflow {
background-color: var(--background-normal-alt);
}
.tox .tox-pop__dialog,
.tox:not([dir='rtl']) .tox-toolbar__group:not(:last-of-type),
.tox .tox-collection--list .tox-collection__group {
border-color: var(--border-normal);
}
.tox .tox-insert-table-picker__label {
color: var(--foreground-normal);
}
.tox .tox-insert-table-picker > div {
border-color: var(--border-normal);
}
.tox .tox-insert-table-picker .tox-insert-table-picker__selected {
border-color: var(--primary);
}
.tox .tox-pop.tox-pop--top::after {
border-bottom-color: var(--background-subdued);
}
.tox .tox-pop.tox-pop--top::before {
border-bottom-color: var(--border-normal);
}
.tox .tox-dialog-wrap__backdrop .tox-rgba-preview {
visibility: hidden;
}
@media screen and (max-width: 767px) {
.tox .tox-dialog__body-nav-item {
text-align: center;
}
}
}

View File

@@ -134,7 +134,7 @@
rounded
icon
:loading="saving"
:disabled="saveAllowed === false || hasEdits === false"
:disabled="isSavable === false"
v-tooltip.bottom="saveAllowed ? $t('save') : $t('not_allowed')"
@click="saveAndQuit"
>
@@ -143,7 +143,7 @@
<template #append-outer>
<save-options
v-if="collectionInfo.meta && collectionInfo.meta.singleton !== true"
:disabled="hasEdits === false"
:disabled="isSavable === false"
@save-and-stay="saveAndStay"
@save-and-add-new="saveAndAddNew"
@save-as-copy="saveAsCopyAndNavigate"
@@ -267,7 +267,7 @@ export default defineComponent({
const revisionsDrawerDetail = ref<Vue | null>(null);
const { info: collectionInfo, primaryKeyField } = useCollection(collection);
const { info: collectionInfo, defaults, primaryKeyField } = useCollection(collection);
const {
isNew,
@@ -288,7 +288,24 @@ export default defineComponent({
validationErrors,
} = useItem(collection, primaryKey);
const hasEdits = computed<boolean>(() => Object.keys(edits.value).length > 0);
const hasEdits = computed(() => Object.keys(edits.value).length > 0);
const isSavable = computed(() => {
if (saveAllowed.value === false) return false;
if (
!primaryKeyField.value?.schema?.has_auto_increment &&
!primaryKeyField.value?.meta?.special?.includes('uuid')
) {
return !!edits.value?.[primaryKeyField.value.field];
}
if (isNew.value === true) {
return Object.keys(defaults.value).length > 0 || hasEdits.value;
}
return hasEdits.value;
});
const confirmDelete = ref(false);
const confirmArchive = ref(false);
@@ -341,7 +358,7 @@ export default defineComponent({
error,
isNew,
edits,
hasEdits,
isSavable,
saving,
collectionInfo,
saveAndQuit,
@@ -387,14 +404,14 @@ export default defineComponent({
}
async function saveAndQuit() {
if (saveAllowed.value === false || hasEdits.value === false) return;
if (isSavable.value === false) return;
await save();
if (props.singleton === false) router.push(`/collections/${props.collection}`);
}
async function saveAndStay() {
if (saveAllowed.value === false || hasEdits.value === false) return;
if (isSavable.value === false) return;
const savedItem: Record<string, any> = await save();
@@ -408,7 +425,7 @@ export default defineComponent({
}
async function saveAndAddNew() {
if (saveAllowed.value === false || hasEdits.value === false) return;
if (isSavable.value === false) return;
await save();

View File

@@ -67,6 +67,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
if (inter.system === true) return false;
const matchesType = inter.types.includes(state.fieldData?.type || 'alias');
const matchesLocalType = inter.localTypes?.includes(type);
let matchesRelation = false;
if (type === 'standard' || type === 'presentation') {
@@ -81,7 +82,7 @@ function initLocalStore(collection: string, field: string, type: typeof localTyp
matchesRelation = inter.relationship === type;
}
return matchesType && matchesRelation;
return matchesType && matchesRelation && (matchesLocalType === undefined || matchesLocalType);
})
.sort((a, b) => (a.name > b.name ? 1 : -1));
});

View File

@@ -144,7 +144,7 @@ export default defineComponent({
params.filter.role = { _eq: props.role };
}
const response = await api.get('/permissions', params);
const response = await api.get('/permissions', { params });
permissions.value = response.data.data;
} catch (err) {

View File

@@ -4,7 +4,7 @@ import LogoutRoute from '@/routes/logout';
import ResetPasswordRoute from '@/routes/reset-password';
import { refresh } from '@/auth';
import { hydrate } from '@/hydrate';
import { useAppStore, useUserStore, useSettingsStore } from '@/stores/';
import { useAppStore, useUserStore, useServerStore } from '@/stores/';
import PrivateNotFoundRoute from '@/routes/private-not-found';
import getRootPath from '@/utils/get-root-path';
@@ -81,7 +81,7 @@ export function replaceRoutes(routeFilter: (routes: RouteConfig[]) => RouteConfi
export const onBeforeEach: NavigationGuard = async (to, from, next) => {
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const serverStore = useServerStore();
// First load
if (from.name === null) {
@@ -91,8 +91,8 @@ export const onBeforeEach: NavigationGuard = async (to, from, next) => {
} catch {}
}
if (settingsStore.state.settings === null) {
await settingsStore.hydrate();
if (serverStore.state.info === null) {
await serverStore.hydrate();
}
if (to.meta?.public !== true && appStore.state.hydrated === false) {

View File

@@ -25,7 +25,7 @@
import { defineComponent, computed, PropType } from '@vue/composition-api';
import LoginForm from './components/login-form/';
import ContinueAs from './components/continue-as/';
import { useAppStore, useSettingsStore } from '@/stores';
import { useAppStore } from '@/stores';
import { LogoutReason } from '@/auth';
@@ -43,12 +43,10 @@ export default defineComponent({
components: { LoginForm, ContinueAs },
setup() {
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const authenticated = computed(() => appStore.state.authenticated);
const currentProject = computed(() => settingsStore.state.settings);
return { authenticated, currentProject };
return { authenticated };
},
});
</script>

View File

@@ -7,5 +7,6 @@ export * from './permissions';
export * from './presets';
export * from './relations';
export * from './requests';
export * from './server';
export * from './settings';
export * from './user';

View File

@@ -2,6 +2,7 @@ import { createStore } from 'pinia';
import { Preset } from '@/types';
import { useUserStore } from '@/stores/';
import api from '@/api';
import { nanoid } from 'nanoid';
const defaultPreset: Omit<Preset, 'collection'> = {
bookmark: null,
@@ -14,6 +15,8 @@ const defaultPreset: Omit<Preset, 'collection'> = {
layout_options: null,
};
let currentUpdate: Record<number, string> = {};
export const usePresetsStore = createStore({
id: 'presetsStore',
state: () => ({
@@ -60,17 +63,22 @@ export const usePresetsStore = createStore({
return response.data.data;
},
async update(id: number, updates: Partial<Preset>) {
const updateID = nanoid();
currentUpdate[id] = updateID;
const response = await api.patch(`/presets/${id}`, updates);
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
const updatedPreset = response.data.data;
if (currentUpdate[id] === updateID) {
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
const updatedPreset = response.data.data;
if (preset.id === updatedPreset.id) {
return updatedPreset;
}
if (preset.id === updatedPreset.id) {
return updatedPreset;
}
return preset;
});
return preset;
});
}
return response.data.data;
},

43
app/src/stores/server.ts Normal file
View File

@@ -0,0 +1,43 @@
import { createStore } from 'pinia';
import api from '@/api';
type Info = {
project: null | {
project_name: string | null;
project_logo: string | null;
project_color: string | null;
public_foreground: string | null;
public_background: string | null;
public_note: string | null;
custom_css: string | null;
};
directus?: {
version: string;
};
node?: {
version: string;
uptime: number;
};
os?: {
type: string;
version: string;
uptime: number;
totalmem: number;
};
};
export const useServerStore = createStore({
id: 'serverStore',
state: () => ({
info: null as null | Info,
}),
actions: {
async hydrate() {
const response = await api.get(`/server/info`);
this.state.info = response.data.data;
},
dehydrate() {
this.reset();
},
},
});

View File

@@ -28,7 +28,7 @@ export default async function uploadFile(
try {
let response = null;
if (options?.fileId !== undefined) {
if (options?.fileId) {
response = await api.patch(`/files/${options.fileId}`, formData, {
onUploadProgress,
});

View File

@@ -1,5 +1,13 @@
<template>
<v-drawer v-model="_active" :title="$t('select_item')" @cancel="cancel">
<template #actions>
<search-input v-model="searchQuery" />
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</template>
<component
:is="`layout-${localLayout}`"
:collection="collection"
@@ -7,6 +15,7 @@
:filters="filters"
:layout-query.sync="localQuery"
:layout-options.sync="localOptions"
:search-query="searchQuery"
@update:selection="onSelect"
select-mode
class="layout"
@@ -19,12 +28,6 @@
<v-info :title="$tc('item_count', 0)" :icon="collectionInfo.icon" center />
</template>
</component>
<template #actions>
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
<v-icon name="check" />
</v-button>
</template>
</v-drawer>
</template>
@@ -33,8 +36,10 @@ import { defineComponent, PropType, ref, computed, toRefs, onUnmounted } from '@
import { Filter } from '@/types';
import usePreset from '@/composables/use-preset';
import useCollection from '@/composables/use-collection';
import SearchInput from '@/views/private/components/search-input';
export default defineComponent({
components: { SearchInput },
props: {
active: {
type: Boolean,
@@ -65,7 +70,7 @@ export default defineComponent({
const { collection } = toRefs(props);
const { info: collectionInfo } = useCollection(collection);
const { layout, layoutOptions, layoutQuery } = usePreset(collection);
const { layout, layoutOptions, layoutQuery, searchQuery } = usePreset(collection);
// This is a local copy of the layout. This means that we can sync it the layout without
// having use-preset auto-save the values
@@ -73,7 +78,18 @@ export default defineComponent({
const localOptions = ref(layoutOptions.value);
const localQuery = ref(layoutQuery.value);
return { save, cancel, _active, _selection, onSelect, localLayout, localOptions, localQuery, collectionInfo };
return {
save,
cancel,
_active,
_selection,
onSelect,
localLayout,
localOptions,
localQuery,
collectionInfo,
searchQuery,
};
function useActiveState() {
const localActive = ref(false);

View File

@@ -8,16 +8,16 @@
<script lang="ts">
import { defineComponent, computed } from '@vue/composition-api';
import LatencyIndicator from '../latency-indicator';
import { useSettingsStore, useLatencyStore } from '@/stores/';
import { useServerStore, useLatencyStore } from '@/stores/';
import { sortBy } from 'lodash';
export default defineComponent({
components: { LatencyIndicator },
setup() {
const latencyStore = useLatencyStore();
const settingsStore = useSettingsStore();
const serverStore = useServerStore();
const name = computed(() => settingsStore.state.settings?.project_name);
const name = computed(() => serverStore.state.info?.project?.project_name);
return { name };
},

View File

@@ -1,5 +1,5 @@
<template>
<div class="public-view" :class="{ branded : isBranded }">
<div class="public-view" :class="{ branded: isBranded }">
<div class="container" :class="{ wide }">
<div class="title-box">
<div
@@ -32,7 +32,7 @@
<script lang="ts">
import { version } from '../../../package.json';
import { defineComponent, computed } from '@vue/composition-api';
import { useSettingsStore } from '@/stores';
import { useServerStore } from '@/stores';
import marked from 'marked';
import getRootPath from '../../utils/get-root-path';
@@ -44,21 +44,21 @@ export default defineComponent({
},
},
setup() {
const settingsStore = useSettingsStore();
const serverStore = useServerStore();
const isBranded = computed(() => {
return (settingsStore.state.settings?.project_color) ? true : false;
return serverStore.state.info?.project?.project_color ? true : false;
});
const backgroundStyles = computed<string>(() => {
const defaultColor = '#263238';
if (settingsStore.state.settings?.public_background) {
const url = getRootPath() + `assets/${settingsStore.state.settings.public_background}`;
if (serverStore.state.info?.project?.public_background) {
const url = getRootPath() + `assets/${serverStore.state.info.project?.public_background}`;
return `url(${url})`;
}
return settingsStore.state.settings?.project_color || defaultColor;
return serverStore.state.info?.project?.project_color || defaultColor;
});
const artStyles = computed(() => ({
@@ -68,16 +68,16 @@ export default defineComponent({
}));
const foregroundURL = computed(() => {
if (!settingsStore.state.settings?.public_foreground) return null;
return getRootPath() + `assets/${settingsStore.state.settings.public_foreground}`;
if (!serverStore.state.info?.project?.public_foreground) return null;
return getRootPath() + `assets/${serverStore.state.info.project?.public_foreground}`;
});
const logoURL = computed<string | null>(() => {
if (!settingsStore.state.settings?.project_logo) return null;
return getRootPath() + `assets/${settingsStore.state.settings.project_logo}`;
if (!serverStore.state.info?.project?.project_logo) return null;
return getRootPath() + `assets/${serverStore.state.info.project?.project_logo}`;
});
return { version, artStyles, marked, settings: settingsStore.state.settings, foregroundURL, logoURL, isBranded };
return { version, artStyles, marked, settings: serverStore.state.info, foregroundURL, logoURL, isBranded };
},
});
</script>

7
package-lock.json generated
View File

@@ -15912,6 +15912,7 @@
"lodash": "^4.17.19",
"macos-release": "^2.4.1",
"memcached": "^2.2.2",
"mime-types": "^2.1.27",
"ms": "^2.1.2",
"mssql": "^6.2.0",
"mysql": "^2.18.1",
@@ -15925,6 +15926,7 @@
"pg": "^8.4.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",
@@ -15987,6 +15989,11 @@
"integrity": "sha512-64/bYByMrhWULUaCd+6/72c9PMWhiVFs3EVxl9Ct6a3v/U8+rKgqP2w+kKg/BIGgMJyB+Bk/eNivT32Al+Jghw==",
"optional": true
},
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
},
"uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",