diff options
| author | Aubrey-Li <63608597+Aubrey-Li@users.noreply.github.com> | 2021-08-03 16:57:25 -0700 |
|---|---|---|
| committer | Aubrey-Li <63608597+Aubrey-Li@users.noreply.github.com> | 2021-08-03 16:57:25 -0700 |
| commit | faa2449d0667507b5424984c0f1d70886d0cb025 (patch) | |
| tree | 02a984b9b8fb2b8e3c4c93f102d9c4b700ff0fda /src/fields | |
| parent | f4b910cc51c7c6b9c794d1b59198d132af3580b7 (diff) | |
| parent | 0e8aef275346b4ba3bc1bb91fda17a335c307bf1 (diff) | |
Merge branch 'master' into trails-aubrey
Diffstat (limited to 'src/fields')
| -rw-r--r-- | src/fields/Doc.ts | 80 | ||||
| -rw-r--r-- | src/fields/URLField.ts | 15 | ||||
| -rw-r--r-- | src/fields/util.ts | 29 |
3 files changed, 83 insertions, 41 deletions
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 3b8109385..c759bae5a 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -21,8 +21,9 @@ import { listSpec } from "./Schema"; import { ComputedField, ScriptField } from "./ScriptField"; import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from "./URLField"; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; +import { deleteProperty, GetEffectiveAcl, getField, getter, inheritParentAcls, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); +import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; export namespace Field { @@ -61,10 +62,10 @@ export type FieldWaiting<T extends RefField = RefField> = T extends undefined ? export type FieldResult<T extends Field = Field> = Opt<T> | FieldWaiting<Extract<T, RefField>>; /** - * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs. - * If a default value is given, that will be returned instead of undefined. - * If a default value is given, the returned value should not be modified as it might be a temporary value. - * If no default value is given, and the returned value is not undefined, it can be safely modified. + * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs. + * If a default value is given, that will be returned instead of undefined. + * If a default value is given, the returned value should not be modified as it might be a temporary value. + * If no default value is given, and the returned value is not undefined, it can be safely modified. */ export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>; export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>; @@ -251,7 +252,8 @@ export class Doc extends RefField { DocServer.GetRefField(this[Id], true); } }; - if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { + const writeMode = DocServer.getFieldWriteMode(fKey as string); + if (fKey.startsWith("acl") || writeMode !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; await fn(); } else { @@ -365,13 +367,13 @@ export namespace Doc { /** * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies * the values of the properties of a source object into the target. - * + * * This is just a specific, Dash-authored version that serves the same role for our * Doc class. - * - * @param doc the target document into which you'd like to insert the new fields + * + * @param doc the target document into which you'd like to insert the new fields * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key - * to a potentially undefined field, where each entry in this mapping is optional. + * to a potentially undefined field, where each entry in this mapping is optional. */ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>, skipUndefineds: boolean = false, isInitializing = false) { isInitializing && (doc[Initializing] = true); @@ -398,7 +400,7 @@ export namespace Doc { } // Gets the data document for the document. Note: this is mis-named -- it does not specifically - // return the doc's proto, but rather recursively searches through the proto inheritance chain + // return the doc's proto, but rather recursively searches through the proto inheritance chain // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). export function GetProto(doc: Doc): Doc { if (doc instanceof Promise) { @@ -424,6 +426,9 @@ export namespace Doc { return Array.from(results); } + /** + * @returns the index of doc toFind in list of docs, -1 otherwise + */ export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) { let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1); index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1); @@ -536,7 +541,7 @@ export namespace Doc { const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); !dontCreate && assignKey(new List<Doc>(clones)); } else if (doc[key] instanceof Doc) { - assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields + assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields } else { !dontCreate && assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { @@ -562,7 +567,7 @@ export namespace Doc { } else if (field instanceof ObjectField) { await copyObjectField(field); } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + debugger; //This shouldn't happen... } else { assignKey(field); } @@ -597,7 +602,7 @@ export namespace Doc { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; - const regex = `(${Utils.prepend("/doc/")})([^"]*)`; + const regex = `(${Doc.localServerPath()})([^"]*)`; const re = new RegExp(regex, "g"); copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); @@ -668,14 +673,14 @@ export namespace Doc { const _pendingMap: Map<string, boolean> = new Map(); // // Returns an expanded template layout for a target data document if there is a template relationship - // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties + // between the two. If so, the layoutDoc is expanded into a new document that inherits the properties // of the original layout while allowing for individual layout properties to be overridden in the expanded layout. // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key. // NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and // the derefence will then occur on the rootDocument (the original document). // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as: - // layout_mytemplate(somparam=somearg). + // layout_mytemplate(somparam=somearg). // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) { const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS); @@ -776,7 +781,7 @@ export namespace Doc { copy[key] = cfield[Copy]();// ComputedField.MakeFunction(cfield.script.originalScript); } else if (field instanceof ObjectField) { copy[key] = doc[key] instanceof Doc ? - key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields + key.includes("layout[") ? undefined : doc[key] : // reference documents except remove documents that are expanded teplate fields ObjectField.MakeCopy(field); } else if (field instanceof Promise) { debugger; //This shouldn't happend... @@ -898,6 +903,16 @@ export namespace Doc { return true; } + + // converts a document id to a url path on the server + export function globalServerPath(doc: Doc | string = ""): string { + return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc)); + } + // converts a document id to a url path on the server + export function localServerPath(doc?: Doc): string { + return "/doc/" + (doc ? doc[Id] : ""); + } + export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { const doc2Layout = Doc.Layout(doc2); const doc1Layout = Doc.Layout(doc1); @@ -929,7 +944,7 @@ export namespace Doc { } // the document containing the view layout information - will be the Document itself unless the Document has - // a layout field or 'layout' is given. + // a layout field or 'layout' is given. export function Layout(doc: Doc, layout?: Doc): Doc { const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null); return overrideLayout || doc[LayoutSym] || doc; @@ -1108,7 +1123,7 @@ export namespace Doc { } // filters document in a container collection: - // all documents with the specified value for the specified key are included/excluded + // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: "remove" | "match" | "check" | "x", toggle?: boolean, fieldSuffix?: string, append: boolean = true) { if (!container) return; @@ -1179,6 +1194,9 @@ export namespace Doc { dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; Doc.SetInPlace(ndoc, "title", ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(), true); } + + if (ndoc) inheritParentAcls(CurrentUserUtils.ActiveDashboard, ndoc); + return ndoc; } export function delegateDragFactory(dragFactory: Doc) { @@ -1233,39 +1251,39 @@ export namespace Doc { /** * This function takes any valid JSON(-like) data, i.e. parsed or unparsed, and at arbitrarily * deep levels of nesting, converts the data and structure into nested documents with the appropriate fields. - * + * * After building a hierarchy within / below a top-level document, it then returns that top-level parent. - * + * * If we've received a string, treat it like valid JSON and try to parse it into an object. If this fails, the * string is invalid JSON, so we should assume that the input is the result of a JSON.parse() * call that returned a regular string value to be stored as a Field. - * + * * If we've received something other than a string, since the caller might also pass in the results of a * JSON.parse() call, valid input might be an object, an array (still typeof object), a boolean or a number. * Anything else (like a function, etc. passed in naively as any) is meaningless for this operation. - * + * * All TS/JS objects get converted directly to documents, directly preserving the key value structure. Everything else, * lacking the key value structure, gets stored as a field in a wrapper document. - * + * * @param data for convenience and flexibility, either a valid JSON string to be parsed, * or the result of any JSON.parse() call. * @param title an optional title to give to the highest parent document in the hierarchy. * If whether this function creates a new document or appendToExisting is specified and that document already has a title, * because this title field can be left undefined for the opposite behavior, including a title will overwrite the existing title. * @param appendToExisting **if specified**, there are two cases, both of which return the target document: - * + * * 1) the json to be converted can be represented as a document, in which case the target document will act as the root * of the tree and receive all the conversion results as new fields on itself * 2) the json can't be represented as a document, in which case the function will assign the field-level conversion * results to either the specified key on the target document, or to its "json" key by default. - * + * * If not specified, the function creates and returns a new entirely generic document (different from the Doc.Create calls) * to act as the root of the tree. - * + * * One might choose to specify this field if you want to write to a document returned from a Document.Create function call, * say a TreeView document that will be rendered, not just an untyped, identityless doc that would otherwise be created * from a default call to new Doc. - * + * * @param excludeEmptyObjects whether non-primitive objects (TypeScript objects and arrays) should be converted even * if they contain no data. By default, empty objects and arrays are ignored. */ @@ -1301,7 +1319,7 @@ export namespace Doc { * For each value of the object, recursively convert it to its appropriate field value * and store the field at the appropriate key in the document if it is not undefined * @param object the object to convert - * @returns the object mapped from JSON to field values, where each mapping + * @returns the object mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertObject) */ const convertObject = (object: any, excludeEmptyObjects: boolean, title?: string, target?: Doc): Opt<Doc> => { @@ -1325,10 +1343,10 @@ export namespace Doc { }; /** - * For each element in the list, recursively convert it to a document or other field + * For each element in the list, recursively convert it to a document or other field * and push the field to the list if it is not undefined * @param list the list to convert - * @returns the list mapped from JSON to field values, where each mapping + * @returns the list mapped from JSON to field values, where each mapping * might involve arbitrary recursion (since toField might itself call convertList) */ const convertList = (list: Array<any>, excludeEmptyObjects: boolean): Opt<List<Field>> => { diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 48535edf0..4d3776a2c 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -3,14 +3,17 @@ import { serializable, custom } from "serializr"; import { ObjectField } from "./ObjectField"; import { ToScriptString, ToString, Copy } from "./FieldSymbols"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; +import { Utils } from "../Utils"; function url() { return custom( function (value: URL) { - return value.href; + return value.origin === window.location.origin ? + value.pathname : + value.href; }, function (jsonValue: string) { - return new URL(jsonValue); + return new URL(jsonValue, window.location.origin); } ); } @@ -24,15 +27,21 @@ export abstract class URLField extends ObjectField { constructor(url: URL | string) { super(); if (typeof url === "string") { - url = new URL(url); + url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin); } this.url = url; } [ToScriptString]() { + if (Utils.prepend(this.url.pathname) === this.url.href) { + return `new ${this.constructor.name}("${this.url.pathname}")`; + } return `new ${this.constructor.name}("${this.url.href}")`; } [ToString]() { + if (Utils.prepend(this.url.pathname) === this.url.href) { + return this.url.pathname; + } return this.url.href; } diff --git a/src/fields/util.ts b/src/fields/util.ts index ea91cc057..526e5af72 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -131,6 +131,17 @@ export function denormalizeEmail(email: string) { // playgroundMode = !playgroundMode; // } + +/** + * Copies parent's acl fields to the child + */ +export function inheritParentAcls(parent: Doc, child: Doc) { + const dataDoc = parent[DataSym]; + for (const key of Object.keys(dataDoc)) { + key.startsWith("acl") && distributeAcls(key, dataDoc[key], child); + } +} + /** * These are the various levels of access a user can have to a document. * @@ -215,7 +226,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection) * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change */ -export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[]) { +export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) { if (!visited) visited = [] as Doc[]; if (visited.includes(target)) return; visited.push(target); @@ -236,6 +247,12 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) { target[key] = acl; layoutDocChanged = true; + + if (isDashboard) { + DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => { + docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited)); + }); + } } if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) { @@ -245,28 +262,26 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc dataDocChanged = true; } - // maps over the aliases of the document + // maps over the links of the document const links = DocListCast(dataDoc.links); links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited)); // maps over the children of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).map(d => { - // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? "-all" : "")]).map(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; - if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data) { distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); // maps over the annotations of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + "-annotations"]).map(d => { - // if (GetEffectiveAcl(d) === AclAdmin && (!inheritingFromCollection || !d[key] || HierarchyMapping.get(StrCast(d[key]))! > HierarchyMapping.get(acl)!)) { distributeAcls(key, acl, d, inheritingFromCollection, visited); // } const data = d[DataSym]; - if (data) {// && GetEffectiveAcl(data) === AclAdmin && (!inheritingFromCollection || !data[key] || HierarchyMapping.get(StrCast(data[key]))! > HierarchyMapping.get(acl)!)) { + if (data) { distributeAcls(key, acl, data, inheritingFromCollection, visited); } }); |
