Merge branch 'main' into sdk

This commit is contained in:
rijkvanzanten
2020-10-27 10:56:06 +01:00
24 changed files with 433 additions and 255 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

@@ -6,7 +6,7 @@ import {
Action,
Accountability,
PermissionsAction,
Item,
Item as AnyItem,
Query,
PrimaryKey,
AbstractService,
@@ -26,7 +26,7 @@ import getDefaultValue from '../utils/get-default-value';
import { InvalidPayloadException } from '../exceptions';
import { ForbiddenException } from '../exceptions';
export class ItemsService implements AbstractService {
export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
collection: string;
knex: Knex;
accountability: Accountability | null;
@@ -52,7 +52,7 @@ export class ItemsService implements AbstractService {
const primaryKeyField = await this.schemaInspector.primary(this.collection);
const columns = await this.schemaInspector.columns(this.collection);
let payloads = clone(toArray(data));
let payloads: AnyItem[] = clone(toArray(data));
const savedPrimaryKeys = await this.knex.transaction(async (trx) => {
const payloadService = new PayloadService(this.collection, {
@@ -194,7 +194,7 @@ export class ItemsService implements AbstractService {
return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0];
}
async readByQuery(query: Query): Promise<null | Item | Item[]> {
async readByQuery(query: Query): Promise<null | Partial<Item> | Partial<Item>[]> {
const authorizationService = new AuthorizationService({
accountability: this.accountability,
knex: this.knex,
@@ -210,20 +210,24 @@ export class ItemsService implements AbstractService {
}
const records = await runAST(ast, { knex: this.knex });
return records;
return records as Partial<Item> | Partial<Item>[] | null;
}
readByKey(
keys: PrimaryKey[],
query?: Query,
action?: PermissionsAction
): Promise<null | Item[]>;
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Item>;
): Promise<null | Partial<Item>[]>;
readByKey(
key: PrimaryKey,
query?: Query,
action?: PermissionsAction
): Promise<null | Partial<Item>>;
async readByKey(
key: PrimaryKey | PrimaryKey[],
query: Query = {},
action: PermissionsAction = 'read'
): Promise<null | Item | Item[]> {
): Promise<null | Partial<Item> | Partial<Item>[]> {
query = clone(query);
const primaryKeyField = await this.schemaInspector.primary(this.collection);
const keys = toArray(key);
@@ -260,7 +264,7 @@ export class ItemsService implements AbstractService {
if (result === null) throw new ForbiddenException();
return result;
return result as Partial<Item> | Partial<Item>[] | null;
}
update(data: Partial<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
@@ -277,7 +281,7 @@ export class ItemsService implements AbstractService {
if (data && key) {
const keys = toArray(key);
let payload = clone(data);
let payload: Partial<AnyItem> | Partial<AnyItem>[] = clone(data);
const customProcessed = await emitter.emitAsync(
`${this.eventScope}.update.before`,
@@ -550,11 +554,11 @@ export class ItemsService implements AbstractService {
return await this.delete(keys);
}
async readSingleton(query: Query) {
async readSingleton(query: Query): Promise<Partial<Item>> {
query = clone(query);
query.single = true;
const record = (await this.readByQuery(query)) as Item;
const record = (await this.readByQuery(query)) as Partial<Item>;
if (!record) {
const columns = await this.schemaInspector.columnInfo(this.collection);
@@ -564,7 +568,7 @@ export class ItemsService implements AbstractService {
defaults[column.name] = getDefaultValue(column);
}
return defaults;
return defaults as Partial<Item>;
}
return record;

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

@@ -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

@@ -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];
}

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

@@ -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

@@ -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

@@ -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

@@ -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>