GitLab provider (part 2)

This commit is contained in:
Benoit Schweblin 2018-09-20 10:00:07 +01:00
parent 6b91c2bafb
commit da9b87620f
20 changed files with 129 additions and 108 deletions

View File

@ -219,7 +219,7 @@ export default {
z-index: 1; z-index: 1;
width: 100%; width: 100%;
color: darken($error-color, 10%); color: darken($error-color, 10%);
background-color: transparentize(lighten($error-color, 33%), 0.1); background-color: transparentize(lighten($error-color, 33%), 0.075);
font-size: 0.9em; font-size: 0.9em;
line-height: 1.33; line-height: 1.33;
text-align: center; text-align: center;

View File

@ -64,7 +64,7 @@ let cachedHistoryContextHash;
let revisionsPromise; let revisionsPromise;
let revisionContentPromises; let revisionContentPromises;
const pageSize = 30; const pageSize = 30;
const spacerThreshold = 60 * 60 * 1000; // 1h const spacerThreshold = 6 * 60 * 60 * 1000; // 6h
export default { export default {
components: { components: {
@ -129,8 +129,8 @@ export default {
if (fileSyncData && contentSyncData) { if (fileSyncData && contentSyncData) {
return { return {
...historyContext, ...historyContext,
fileSyncData, fileSyncDataId: fileSyncData.id,
contentSyncData, contentSyncDataId: contentSyncData.id,
}; };
} }
} }

View File

@ -9,23 +9,19 @@
<hr> <hr>
<menu-entry @click.native="addCouchdbWorkspace"> <menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<div>CouchDB workspace</div> <span>Add a <b>CouchDB</b> backed workspace</span>
<span>Add a workspace synced with a CouchDB database.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGithubWorkspace"> <menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<div>GitHub workspace</div> <span>Add a <b>GitHub</b> backed workspace</span>
<span>Add a workspace synced with a GitHub repository.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGitlabWorkspace"> <menu-entry @click.native="addGitlabWorkspace">
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
<div>GitLab workspace</div> <span>Add a <b>GitLab</b> backed workspace</span>
<span>Add a workspace synced with a GitLab project.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace"> <menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<div>Google Drive workspace</div> <span>Add a <b>Google Drive</b> backed workspace</span>
<span>Add a workspace synced with a Google Drive folder.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="manageWorkspaces"> <menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>

View File

@ -6,7 +6,8 @@
</div> </div>
<p>Link your <b>GitLab</b> account to <b>StackEdit</b>.</p> <p>Link your <b>GitLab</b> account to <b>StackEdit</b>.</p>
<form-entry label="GitLab URL" error="serverUrl"> <form-entry label="GitLab URL" error="serverUrl">
<input slot="field" class="textfield" type="text" v-model.trim="serverUrl" @keydown.enter="resolve()"> <input v-if="config.forceServerUrl" slot="field" class="textfield" type="text" disabled="disabled" v-model="config.forceServerUrl">
<input v-else slot="field" class="textfield" type="text" v-model.trim="serverUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> https://gitlab.example.com/ <b>Example:</b> https://gitlab.example.com/
</div> </div>
@ -42,14 +43,15 @@ export default modalTemplate({
}, },
methods: { methods: {
resolve() { resolve() {
if (!this.serverUrl) { const serverUrl = this.config.forceServerUrl || this.serverUrl;
if (!serverUrl) {
this.setError('serverUrl'); this.setError('serverUrl');
} }
if (!this.applicationId) { if (!this.applicationId) {
this.setError('applicationId'); this.setError('applicationId');
} }
if (this.serverUrl && this.applicationId) { if (serverUrl && this.applicationId) {
const parsedUrl = this.serverUrl.match(/^(https:\/\/[^/]+)/); const parsedUrl = serverUrl.match(/^(https:\/\/[^/]+)/);
if (!parsedUrl) { if (!parsedUrl) {
this.setError('serverUrl'); this.setError('serverUrl');
} else { } else {

View File

@ -8,7 +8,7 @@
<form-entry label="Project URL" error="projectUrl"> <form-entry label="Project URL" error="projectUrl">
<input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()"> <input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
<div class="form-entry__info"> <div class="form-entry__info">
<b>Example:</b> {{config.token.serverUrl}}path/to/project <b>Example:</b> {{config.token.serverUrl}}/path/to/project
</div> </div>
</form-entry> </form-entry>
<form-entry label="File path" error="path"> <form-entry label="File path" error="path">

View File

@ -1,7 +1,7 @@
const origin = `${window.location.protocol}//${window.location.host}`; const origin = `${window.location.protocol}//${window.location.host}`;
export default { export default {
cleanTrashAfter: 0 * 24 * 60 * 60 * 1000, // 7 days cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
origin, origin,
oauth2RedirectUri: `${origin}/oauth2/callback`, oauth2RedirectUri: `${origin}/oauth2/callback`,
types: [ types: [

View File

@ -44,15 +44,18 @@ export default class Provider {
* Parse content serialized with serializeContent() * Parse content serialized with serializeContent()
*/ */
static parseContent(serializedContent, id) { static parseContent(serializedContent, id) {
const result = utils.deepCopy(store.state.content.itemsById[id]) || emptyContent(id); let text = serializedContent;
result.text = utils.sanitizeText(serializedContent);
result.history = [];
const extractedData = dataExtractor.exec(serializedContent); const extractedData = dataExtractor.exec(serializedContent);
if (extractedData) { let result;
if (!extractedData) {
// In case stackedit's data has been manually removed, try to restore them
result = utils.deepCopy(store.state.content.itemsById[id]) || emptyContent(id);
} else {
result = emptyContent(id);
try { try {
const serializedData = extractedData[1].replace(/\s/g, ''); const serializedData = extractedData[1].replace(/\s/g, '');
const parsedData = JSON.parse(utils.decodeBase64(serializedData)); const parsedData = JSON.parse(utils.decodeBase64(serializedData));
result.text = utils.sanitizeText(serializedContent.slice(0, extractedData.index)); text = text.slice(0, extractedData.index);
if (parsedData.properties) { if (parsedData.properties) {
result.properties = utils.sanitizeText(parsedData.properties); result.properties = utils.sanitizeText(parsedData.properties);
} }
@ -62,11 +65,15 @@ export default class Provider {
if (parsedData.comments) { if (parsedData.comments) {
result.comments = parsedData.comments; result.comments = parsedData.comments;
} }
result.history = parsedData.history || []; result.history = parsedData.history;
} catch (e) { } catch (e) {
// Ignore // Ignore
} }
} }
result.text = utils.sanitizeText(text);
if (!result.history) {
result.history = [];
}
return utils.addItemHash(result); return utils.addItemHash(result);
} }

View File

@ -194,8 +194,8 @@ export default new Provider({
}, },
}; };
}, },
async listFileRevisions({ token, contentSyncData }) { async listFileRevisions({ token, contentSyncDataId }) {
const body = await couchdbHelper.retrieveDocumentWithRevisions(token, contentSyncData.id); const body = await couchdbHelper.retrieveDocumentWithRevisions(token, contentSyncDataId);
const revisions = []; const revisions = [];
body._revs_info.forEach((revInfo, idx) => { // eslint-disable-line no-underscore-dangle body._revs_info.forEach((revInfo, idx) => { // eslint-disable-line no-underscore-dangle
if (revInfo.status === 'available') { if (revInfo.status === 'available') {
@ -209,19 +209,19 @@ export default new Provider({
}); });
return revisions; return revisions;
}, },
async loadFileRevision({ token, contentSyncData, revision }) { async loadFileRevision({ token, contentSyncDataId, revision }) {
if (revision.loaded) { if (revision.loaded) {
return false; return false;
} }
const body = await couchdbHelper.retrieveDocument(token, contentSyncData.id, revision.id); const body = await couchdbHelper.retrieveDocument(token, contentSyncDataId, revision.id);
revision.sub = body.sub; revision.sub = body.sub;
revision.created = body.time; revision.created = body.time;
revision.loaded = true; revision.loaded = true;
return true; return true;
}, },
async getFileRevisionContent({ token, contentSyncData, revisionId }) { async getFileRevisionContent({ token, contentSyncDataId, revisionId }) {
const body = await couchdbHelper const body = await couchdbHelper
.retrieveDocumentWithAttachments(token, contentSyncData.id, revisionId); .retrieveDocumentWithAttachments(token, contentSyncDataId, revisionId);
return Provider.parseContent(body.attachments.data, body.item.id); return Provider.parseContent(body.attachments.data, body.item.id);
}, },
}); });

View File

@ -46,12 +46,19 @@ export default new Provider({
async initWorkspace() { async initWorkspace() {
const { owner, repo, branch } = utils.queryParams; const { owner, repo, branch } = utils.queryParams;
const workspaceParams = this.getWorkspaceParams({ owner, repo, branch }); const workspaceParams = this.getWorkspaceParams({ owner, repo, branch });
if (!branch) {
workspaceParams.branch = 'master';
}
// Extract path param
const path = (utils.queryParams.path || '') const path = (utils.queryParams.path || '')
.trim()
.replace(/^\/*/, '') // Remove leading `/` .replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, '/'); // Add trailing `/` .replace(/\/*$/, '/'); // Add trailing `/`
if (path !== '/') { if (path !== '/') {
workspaceParams.path = path; workspaceParams.path = path;
} }
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
const workspace = store.getters['workspace/workspacesById'][workspaceId]; const workspace = store.getters['workspace/workspacesById'][workspaceId];
@ -217,14 +224,14 @@ export default new Provider({
}, },
}; };
}, },
async listFileRevisions({ token, fileSyncData }) { async listFileRevisions({ token, fileSyncDataId }) {
const { owner, repo, branch } = store.getters['workspace/currentWorkspace']; const { owner, repo, branch } = store.getters['workspace/currentWorkspace'];
const entries = await githubHelper.getCommits({ const entries = await githubHelper.getCommits({
token, token,
owner, owner,
repo, repo,
sha: branch, sha: branch,
path: getAbsolutePath(fileSyncData), path: getAbsolutePath({ id: fileSyncDataId }),
}); });
return entries.map(({ return entries.map(({
@ -242,11 +249,12 @@ export default new Provider({
const sub = `${githubHelper.subPrefix}:${user.id}`; const sub = `${githubHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date) const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date); || (commit.committer && commit.committer.date)
|| 1;
return { return {
id: sha, id: sha,
sub, sub,
created: date ? new Date(date).getTime() : 1, created: new Date(date).getTime(),
}; };
}); });
}, },
@ -257,14 +265,14 @@ export default new Provider({
async getFileRevisionContent({ async getFileRevisionContent({
token, token,
contentId, contentId,
fileSyncData, fileSyncDataId,
revisionId, revisionId,
}) { }) {
const { data } = await githubHelper.downloadFile({ const { data } = await githubHelper.downloadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
branch: revisionId, branch: revisionId,
path: getAbsolutePath(fileSyncData), path: getAbsolutePath({ id: fileSyncDataId }),
}); });
return Provider.parseContent(data, contentId); return Provider.parseContent(data, contentId);
}, },

View File

@ -135,24 +135,17 @@ export default new Provider({
token, token,
}); });
return entries.map(({ return entries.map((entry) => {
author, const email = entry.author_email || entry.committer_email;
committer, const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
commit, userSvc.addInfo({
sha, id: sub,
}) => { name: entry.author_name || entry.committer_name,
let user; imageUrl: '',
if (author && author.login) { });
user = author; const date = entry.authored_date || entry.committed_date || 1;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${gitlabHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date);
return { return {
id: sha, id: entry.id,
sub, sub,
created: date ? new Date(date).getTime() : 1, created: date ? new Date(date).getTime() : 1,
}; };

View File

@ -45,29 +45,44 @@ export default new Provider({
return getAbsolutePath({ id }); return getAbsolutePath({ id });
}, },
async initWorkspace() { async initWorkspace() {
const { projectPath, branch } = utils.queryParams; const { serverUrl, branch } = utils.queryParams;
const workspaceParams = this.getWorkspaceParams({ projectPath, branch }); const workspaceParams = this.getWorkspaceParams({ serverUrl, branch });
if (!branch) {
workspaceParams.branch = 'master';
}
// Extract project path param
const projectPath = (utils.queryParams.projectPath || '')
.trim()
.replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, ''); // Remove trailing `/`
workspaceParams.projectPath = projectPath;
// Extract path param
const path = (utils.queryParams.path || '') const path = (utils.queryParams.path || '')
.trim()
.replace(/^\/*/, '') // Remove leading `/` .replace(/^\/*/, '') // Remove leading `/`
.replace(/\/*$/, '/'); // Add trailing `/` .replace(/\/*$/, '/'); // Add trailing `/`
if (path !== '/') { if (path !== '/') {
workspaceParams.path = path; workspaceParams.path = path;
} }
const workspaceId = utils.makeWorkspaceId(workspaceParams); const workspaceId = utils.makeWorkspaceId(workspaceParams);
const workspace = store.getters['workspace/workspacesById'][workspaceId]; const workspace = store.getters['workspace/workspacesById'][workspaceId];
// See if we already have a token // See if we already have a token
let token; const sub = workspace ? workspace.sub : utils.queryParams.sub;
if (workspace) { let token = store.getters['data/gitlabTokensBySub'][sub];
// Token sub is in the workspace
token = store.getters['data/gitlabTokensBySub'][workspace.sub];
}
if (!token) { if (!token) {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' }); const { applicationId } = await store.dispatch('modal/open', {
token = await gitlabHelper.addAccount(serverUrl, applicationId); type: 'gitlabAccount',
forceServerUrl: serverUrl,
});
token = await gitlabHelper.addAccount(serverUrl, applicationId, sub);
} }
if (!workspace) { if (!workspace) {
const projectId = await gitlabHelper.getProjectId(token, workspaceParams);
const pathEntries = (path || '').split('/'); const pathEntries = (path || '').split('/');
const projectPathEntries = (projectPath || '').split('/'); const projectPathEntries = (projectPath || '').split('/');
const name = pathEntries[pathEntries.length - 2] // path ends with `/` const name = pathEntries[pathEntries.length - 2] // path ends with `/`
@ -75,6 +90,7 @@ export default new Provider({
store.dispatch('workspace/patchWorkspacesById', { store.dispatch('workspace/patchWorkspacesById', {
[workspaceId]: { [workspaceId]: {
...workspaceParams, ...workspaceParams,
projectId,
id: workspaceId, id: workspaceId,
sub: token.sub, sub: token.sub,
name, name,
@ -178,12 +194,13 @@ export default new Provider({
async uploadWorkspaceContent({ token, content, file }) { async uploadWorkspaceContent({ token, content, file }) {
const path = store.getters.gitPathsByItemId[file.id]; const path = store.getters.gitPathsByItemId[file.id];
const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`; const absolutePath = `${store.getters['workspace/currentWorkspace'].path || ''}${path}`;
const res = await gitlabHelper.uploadFile({ const sha = gitWorkspaceSvc.shaByPath[path];
await gitlabHelper.uploadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
path: absolutePath, path: absolutePath,
content: Provider.serializeContent(content), content: Provider.serializeContent(content),
sha: gitWorkspaceSvc.shaByPath[path], sha,
}); });
// Return new sync data // Return new sync data
@ -192,7 +209,7 @@ export default new Provider({
id: store.getters.gitPathsByItemId[content.id], id: store.getters.gitPathsByItemId[content.id],
type: content.type, type: content.type,
hash: content.hash, hash: content.hash,
sha: res.content.sha, sha,
}, },
fileSyncData: { fileSyncData: {
id: path, id: path,
@ -223,33 +240,26 @@ export default new Provider({
}, },
}; };
}, },
async listFileRevisions({ token, fileSyncData }) { async listFileRevisions({ token, fileSyncDataId }) {
const { projectId, branch } = store.getters['workspace/currentWorkspace']; const { projectId, branch } = store.getters['workspace/currentWorkspace'];
const entries = await gitlabHelper.getCommits({ const entries = await gitlabHelper.getCommits({
token, token,
projectId, projectId,
sha: branch, sha: branch,
path: getAbsolutePath(fileSyncData), path: getAbsolutePath({ id: fileSyncDataId }),
}); });
return entries.map(({ return entries.map((entry) => {
author, const email = entry.author_email || entry.committer_email;
committer, const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
commit, userSvc.addInfo({
sha, id: sub,
}) => { name: entry.author_name || entry.committer_name,
let user; imageUrl: '', // No way to get user's avatar url...
if (author && author.login) { });
user = author; const date = entry.authored_date || entry.committed_date || 1;
} else if (committer && committer.login) {
user = committer;
}
const sub = `${gitlabHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date);
return { return {
id: sha, id: entry.id,
sub, sub,
created: date ? new Date(date).getTime() : 1, created: date ? new Date(date).getTime() : 1,
}; };
@ -262,14 +272,14 @@ export default new Provider({
async getFileRevisionContent({ async getFileRevisionContent({
token, token,
contentId, contentId,
fileSyncData, fileSyncDataId,
revisionId, revisionId,
}) { }) {
const { data } = await gitlabHelper.downloadFile({ const { data } = await gitlabHelper.downloadFile({
...store.getters['workspace/currentWorkspace'], ...store.getters['workspace/currentWorkspace'],
token, token,
branch: revisionId, branch: revisionId,
path: getAbsolutePath(fileSyncData), path: getAbsolutePath({ id: fileSyncDataId }),
}); });
return Provider.parseContent(data, contentId); return Provider.parseContent(data, contentId);
}, },

View File

@ -167,8 +167,8 @@ export default new Provider({
}, },
}; };
}, },
async listFileRevisions({ token, contentSyncData }) { async listFileRevisions({ token, contentSyncDataId }) {
const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncData.id); const revisions = await googleHelper.getAppDataFileRevisions(token, contentSyncDataId);
return revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
@ -179,9 +179,9 @@ export default new Provider({
// Revisions are already loaded // Revisions are already loaded
return false; return false;
}, },
async getFileRevisionContent({ token, contentSyncData, revisionId }) { async getFileRevisionContent({ token, contentSyncDataId, revisionId }) {
const content = await googleHelper const content = await googleHelper
.downloadAppDataFileRevision(token, contentSyncData.id, revisionId); .downloadAppDataFileRevision(token, contentSyncDataId, revisionId);
return JSON.parse(content); return JSON.parse(content);
}, },
}); });

View File

@ -508,8 +508,8 @@ export default new Provider({
}, },
}; };
}, },
async listFileRevisions({ token, fileSyncData }) { async listFileRevisions({ token, fileSyncDataId }) {
const revisions = await googleHelper.getFileRevisions(token, fileSyncData.id); const revisions = await googleHelper.getFileRevisions(token, fileSyncDataId);
return revisions.map(revision => ({ return revisions.map(revision => ({
id: revision.id, id: revision.id,
sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`, sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
@ -523,10 +523,10 @@ export default new Provider({
async getFileRevisionContent({ async getFileRevisionContent({
token, token,
contentId, contentId,
fileSyncData, fileSyncDataId,
revisionId, revisionId,
}) { }) {
const content = await googleHelper.downloadFileRevision(token, fileSyncData.id, revisionId); const content = await googleHelper.downloadFileRevision(token, fileSyncDataId, revisionId);
return Provider.parseContent(content, contentId); return Provider.parseContent(content, contentId);
}, },
}); });

View File

@ -88,8 +88,8 @@ export default {
store.dispatch('data/addGitlabToken', token); store.dispatch('data/addGitlabToken', token);
return token; return token;
}, },
addAccount(serverUrl, applicationId) { addAccount(serverUrl, applicationId, sub = null) {
return this.startOauth2(serverUrl, applicationId); return this.startOauth2(serverUrl, applicationId, sub);
}, },
/** /**
@ -133,7 +133,7 @@ export default {
path, path,
}) { }) {
return request(token, { return request(token, {
url: `projects/${encodeURIComponent(projectId)}/repository/tree`, url: `projects/${encodeURIComponent(projectId)}/repository/commits`,
params: { params: {
ref_name: branch, ref_name: branch,
path, path,

View File

@ -23,7 +23,7 @@ export default {
if (!loginToken) { if (!loginToken) {
return null; return null;
} }
const loginType = store.getters['workspace/loginToken']; const loginType = store.getters['workspace/loginType'];
const prefix = subPrefixesByType[loginType]; const prefix = subPrefixesByType[loginType];
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub; return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
}, },
@ -44,7 +44,7 @@ export default {
const [type, sub] = parseUserId(userId); const [type, sub] = parseUserId(userId);
// Try to find a token with this sub to resolve name as soon as possible // Try to find a token with this sub to resolve name as soon as possible
const token = store.getters[`data/${type}TokensBySub`][sub]; const token = store.getters['data/tokensByType'][type][sub];
if (token) { if (token) {
store.commit('userInfo/addItem', { store.commit('userInfo/addItem', {
id: userId, id: userId,

View File

@ -94,11 +94,15 @@ export default {
}, },
sanitizeName(name) { sanitizeName(name) {
return `${name || ''}` return `${name || ''}`
// Replace `/`, control characters and other kind of spaces with a space
.replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ').trim() // eslint-disable-line no-control-regex
// Keep only 250 characters // Keep only 250 characters
.slice(0, 250) || constants.defaultName; .slice(0, 250) || constants.defaultName;
}, },
sanitizeFilename(name) {
return this.sanitizeName(`${name || ''}`
// Replace `/`, control characters and other kind of spaces with a space
.replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ') // eslint-disable-line no-control-regex
.trim()) || constants.defaultName;
},
deepCopy, deepCopy,
serializeObject(obj) { serializeObject(obj) {
return obj === undefined ? obj : JSON.stringify(obj, (key, value) => { return obj === undefined ? obj : JSON.stringify(obj, (key, value) => {
@ -128,6 +132,7 @@ export default {
return array.cl_map(value => alphabet[value % radix]).join(''); return array.cl_map(value => alphabet[value % radix]).join('');
}, },
hash(str) { hash(str) {
// https://stackoverflow.com/a/7616484/1333165
let hash = 0; let hash = 0;
if (!str) return hash; if (!str) return hash;
for (let i = 0; i < str.length; i += 1) { for (let i = 0; i < str.length; i += 1) {

View File

@ -20,7 +20,7 @@ export default {
const id = utils.uid(); const id = utils.uid();
const item = { const item = {
id, id,
name: utils.sanitizeName(name), name: utils.sanitizeFilename(name),
parentId: parentId || null, parentId: parentId || null,
}; };
const content = { const content = {
@ -72,7 +72,7 @@ export default {
*/ */
async storeItem(item) { async storeItem(item) {
const id = item.id || utils.uid(); const id = item.id || utils.uid();
const sanitizedName = utils.sanitizeName(item.name); const sanitizedName = utils.sanitizeFilename(item.name);
if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) { if (item.type === 'folder' && forbiddenFolderNameMatcher.exec(sanitizedName)) {
await store.dispatch('modal/open', { await store.dispatch('modal/open', {
@ -125,7 +125,7 @@ export default {
item.parentId = patch.parentId || null; item.parentId = patch.parentId || null;
} }
if (patch.name) { if (patch.name) {
const sanitizedName = utils.sanitizeName(patch.name); const sanitizedName = utils.sanitizeFilename(patch.name);
if (item.type !== 'folder' || !forbiddenFolderNameMatcher.exec(sanitizedName)) { if (item.type !== 'folder' || !forbiddenFolderNameMatcher.exec(sanitizedName)) {
item.name = sanitizedName; item.name = sanitizedName;
} }

View File

@ -186,7 +186,7 @@ textarea {
&[disabled] { &[disabled] {
cursor: not-allowed; cursor: not-allowed;
background-color: #f2f2f2; background-color: #f0f0f0;
color: #999; color: #999;
} }
} }

View File

@ -85,12 +85,12 @@ samp {
blockquote { blockquote {
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
padding-left: 1.5em; padding-left: 1.5em;
border-left: 5px solid rgba(0, 0, 0, 0.075); border-left: 5px solid rgba(0, 0, 0, 0.1);
.app--dark .layout__panel--editor &, .app--dark .layout__panel--editor &,
.app--dark .layout__panel--preview & { .app--dark .layout__panel--preview & {
color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 0.4);
border-left-color: rgba(255, 255, 255, 0.075); border-left-color: rgba(255, 255, 255, 0.1);
} }
} }

View File

@ -13,7 +13,7 @@ $code-border-radius: 3px;
$link-color: #0c93e4; $link-color: #0c93e4;
$error-color: #f31; $error-color: #f31;
$border-radius-base: 3px; $border-radius-base: 3px;
$hr-color: rgba(128, 128, 128, 0.2); $hr-color: rgba(128, 128, 128, 0.33);
$navbar-bg: #2c2c2c; $navbar-bg: #2c2c2c;
$navbar-color: mix($navbar-bg, #fff, 33%); $navbar-color: mix($navbar-bg, #fff, 33%);
$navbar-hover-color: #fff; $navbar-hover-color: #fff;